Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

When you open Terminal.app on your Mac, you’re not just launching an application—you’re opening a portal to a Unix system that traces its lineage back to the 1970s at Bell Labs. Beneath macOS’s polished graphical interface lies Darwin, a fully certified Unix operating system built on decades of BSD development and NeXT innovation.

This book is your comprehensive guide to understanding and leveraging that Unix foundation.

Who This Book Is For

This book serves several audiences:

Linux and BSD users coming to macOS will find detailed explanations of where macOS diverges from familiar Unix conventions. Why doesn’t sed -i work the same way? Where did /etc/init.d go? Why are there so many .DS_Store files everywhere? This book answers these questions and many more.

Mac users discovering the command line will find a thorough introduction to Unix concepts, contextualized for macOS. You’ll learn not just what commands do, but why macOS implements things the way it does, and how the terminal integrates with the graphical system you already know.

System administrators managing Mac fleets will find practical guidance on automation, configuration management, and security hardening. From understanding launchd to deploying configuration profiles, this book covers enterprise-relevant topics in depth.

Developers building for Apple platforms will understand the toolchain beneath Xcode, learn to compile Unix software on macOS, and navigate the complexities of code signing, notarization, and Apple Silicon.

What Makes macOS Unique

macOS occupies a unique position in the Unix landscape. It’s the only Unix-certified operating system with significant desktop market share. It combines:

  • A BSD userland derived from FreeBSD
  • A hybrid kernel (XNU) blending Mach microkernel concepts with BSD
  • NeXT heritage in its service management and development tools
  • Apple innovations in filesystem design, security architecture, and hardware integration

This combination creates both opportunities and challenges. You have access to a robust Unix toolkit, but many assumptions from Linux or traditional BSD don’t apply. GNU tools behave differently than their BSD counterparts. The filesystem hierarchy has macOS-specific locations. System services use a completely different model than systemd or traditional init.

How This Book Is Organized

The book progresses from foundations to practical applications:

Part I: Foundations & History explains where macOS came from and how its Unix implementation differs from others. Understanding this context helps you reason about why things work the way they do.

Part II: Filesystem & Storage covers APFS, HFS+, extended attributes, and macOS’s unique approach to file organization. You’ll learn to work with—not against—macOS’s filesystem quirks.

Part III: Shell & Terminal addresses the transition from bash to zsh, terminal configuration, and macOS-specific shell features. You’ll learn to create a productive command-line environment.

Part IV: Package Management provides a deep dive into Homebrew and alternatives. You’ll understand not just how to install software, but how these systems work and how to troubleshoot them.

Part V: System Services & Processes explains launchd, Apple’s replacement for init, systemd, and cron combined. You’ll learn to create and manage services the macOS way.

Part VI: Unix Commands & Tools documents the differences between BSD and GNU commands, introduces macOS-specific tools, and helps you write portable scripts.

Part VII: Development Environment covers Xcode Command Line Tools, the LLVM toolchain, library linking, and the complexities of building software on modern macOS.

Part VIII: System Administration addresses user management, permissions, System Integrity Protection, firewalls, and logging—everything you need to manage macOS systems.

Part IX: Networking explains network configuration, Bonjour, VPNs, and sharing services from the command line.

Part X: Security Model demystifies Gatekeeper, code signing, notarization, sandboxing, and the privacy controls that increasingly define the macOS experience.

Part XI: Interoperability helps you work across platforms—sharing files, running containers, writing portable scripts, and connecting to remote systems.

Part XII: Performance & Optimization teaches you to diagnose and resolve performance issues using both GUI and command-line tools.

Conventions Used in This Book

Command Examples

Terminal commands are shown in code blocks with a $ prefix indicating the shell prompt:

$ uname -a
Darwin MacBook-Pro.local 23.0.0 Darwin Kernel Version 23.0.0: Fri Sep 15 14:41:43 PDT 2023; root:xnu-10002.1.13~1/RELEASE_ARM64_T6000 arm64

Commands requiring sudo (administrator privileges) are shown with #:

# systemsetup -setremotelogin on

File Paths

File paths are shown in monospace: /Library/LaunchDaemons/com.example.daemon.plist

When a path begins with ~, it refers to your home directory:

  • ~/.zshrc expands to /Users/yourusername/.zshrc

Admonitions

Throughout the book, you’ll find specially marked sections:

Note: Additional information or context that’s helpful but not critical.

Warning: Important caveats or potential pitfalls to avoid.

Tip: Practical advice for improved workflows.

Version Information

This book is written for macOS Sonoma (14.x) and later, running on both Intel and Apple Silicon Macs. Most content applies to earlier versions, but some features—particularly security and filesystem capabilities—are version-specific. When version matters, it’s noted explicitly.

Prerequisites

This book assumes:

  • You have a Mac running a recent version of macOS
  • You’re comfortable using applications and basic computer operations
  • You have an interest in learning command-line tools

No prior Unix experience is required, though readers with Linux or BSD backgrounds will find much that’s familiar—and much that differs in important ways.

A Note on Terminology

Throughout this book, we use several terms that deserve clarification:

  • macOS: Apple’s desktop operating system, formerly known as Mac OS X and OS X
  • Darwin: The open-source Unix foundation underlying macOS (and iOS, tvOS, watchOS)
  • Unix: The family of operating systems descended from or inspired by the original AT&T Unix
  • BSD: Berkeley Software Distribution, a Unix variant that heavily influences macOS
  • POSIX: Portable Operating System Interface, the standards that define Unix compatibility

When we say macOS is “Unix,” we mean it literally—macOS is UNIX 03 certified by The Open Group, the organization that owns the Unix trademark.

Let’s Begin

The command line on macOS is not a relic of the past—it’s a powerful interface that complements the graphical experience. Whether you’re automating tasks, managing servers, or building software, understanding macOS’s Unix foundations will make you more effective.

Open Terminal.app (you’ll find it in /Applications/Utilities/), and let’s explore what lies beneath the surface.

The Unix Heritage of macOS

macOS is not merely “Unix-like”—it is Unix. Apple’s desktop operating system carries official UNIX 03 certification from The Open Group, making it one of the few consumer operating systems that can legally use the Unix name. But how did a company known for the Macintosh end up shipping a certified Unix system?

The answer involves a journey through computing history, touching Bell Labs, Berkeley, Carnegie Mellon, NeXT, and finally Apple. Understanding this heritage is essential for anyone who wants to truly master macOS’s command-line environment.

The Path to Darwin

macOS’s Unix foundation is called Darwin. Released as open source since 2000, Darwin combines:

  1. The XNU kernel: A hybrid design merging the Mach microkernel with BSD components
  2. A BSD userland: Command-line tools and libraries derived primarily from FreeBSD
  3. Apple frameworks: Extensions and adaptations for macOS-specific functionality

Darwin is not macOS—it lacks the Aqua graphical interface, the Cocoa frameworks, and the proprietary applications that define the Mac experience. But it provides the Unix substrate on which everything else runs.

What You’ll Learn in This Part

This section traces macOS’s Unix lineage and explains how it differs from other Unix-like systems:

Darwin: The Open Source Core introduces Darwin, examining its components, its open-source status, and its relationship to macOS.

From NeXTSTEP to macOS tells the story of how Steve Jobs’s post-Apple company created the technology that would eventually become macOS.

The XNU Kernel Architecture dives deep into macOS’s hybrid kernel, explaining how Mach and BSD components work together.

macOS vs Linux vs BSD: Key Differences provides a practical comparison for users coming from other Unix systems, highlighting where macOS does things differently and why.

POSIX Compliance and Standards examines macOS’s adherence to Unix standards and what this means for portability and compatibility.

Why History Matters

You might wonder why a practical guide spends time on history. The answer is simple: understanding why macOS works the way it does helps you predict how it will behave. When you understand that macOS’s service management comes from NeXT, that its command-line tools come from FreeBSD, and that its kernel blends two distinct traditions, you can reason about unfamiliar situations instead of just memorizing facts.

The next chapters provide that understanding.

Darwin: The Open Source Core

Darwin is the open-source operating system that forms the foundation of macOS, iOS, iPadOS, watchOS, tvOS, and visionOS. While Apple’s consumer products include proprietary components layered on top, Darwin itself has been available under various open-source licenses since its initial release in 2000.

What Is Darwin?

Darwin is a complete Unix operating system, comprising:

  • XNU: The hybrid kernel combining Mach and BSD
  • BSD subsystem: Process management, networking, filesystem interfaces
  • IOKit: The driver framework for hardware interaction
  • Userland tools: Command-line utilities derived from FreeBSD
  • Core libraries: libSystem and related foundational libraries

When you type commands in Terminal.app, you’re interacting directly with Darwin. The graphical macOS experience—Aqua, Finder, the Dock—sits above Darwin but depends entirely on it.

Darwin’s Open Source Status

Darwin’s licensing is complex. Different components use different licenses:

ComponentLicense
XNU kernelApple Public Source License (APSL) 2.0
BSD userlandVarious BSD licenses
IOKitAPSL 2.0
Mach componentsVarious (CMU, Apple)

The Apple Public Source License is OSI-approved but not GPL-compatible. This means Darwin code can’t be directly combined with GPL-licensed projects like Linux.

Accessing Darwin Source Code

Apple publishes Darwin source code at opensource.apple.com. You can browse and download:

# View available Darwin components
$ curl -s https://opensource.apple.com/tarballs/ | grep -o 'href="[^"]*"' | head -20

Key repositories include:

  • xnu: The kernel
  • Libc: The C standard library
  • shell_cmds: Shell built-in commands
  • file_cmds: File manipulation commands (ls, cp, mv, etc.)
  • network_cmds: Networking utilities

Building Darwin

While Apple publishes source code, building a complete Darwin system is challenging. The community-driven PureDarwin project maintains bootable Darwin distributions, though these lack the polish and hardware support of macOS.

Darwin vs macOS

The relationship between Darwin and macOS is analogous to the relationship between the Linux kernel and a Linux distribution like Ubuntu:

DarwinmacOS
Open-source Unix foundationComplete commercial product
Kernel + userland toolsDarwin + Aqua GUI + Apple frameworks + apps
No graphical interfaceFull desktop experience
Freely redistributableLicensed per device

However, there’s a crucial difference: Darwin without macOS is rarely used, while Linux without any particular distribution doesn’t exist as a consumer product.

Examining Your Darwin Installation

You can inspect Darwin’s presence on your system:

# Display Darwin version
$ uname -a
Darwin MacBook-Pro.local 23.4.0 Darwin Kernel Version 23.4.0: Fri Mar 15 00:12:49 PDT 2024; root:xnu-10063.101.17~1/RELEASE_ARM64_T6020 arm64

# View Darwin kernel version details
$ sysctl kern.ostype kern.osrelease kern.version
kern.ostype: Darwin
kern.osrelease: 23.4.0
kern.version: Darwin Kernel Version 23.4.0: Fri Mar 15 00:12:49 PDT 2024; root:xnu-10063.101.17~1/RELEASE_ARM64_T6020

The uname output reveals key information:

  • Darwin: The OS type
  • 23.4.0: The Darwin kernel version (not the macOS version)
  • xnu-10063.101.17~1: The specific XNU build
  • RELEASE_ARM64_T6020: Release build for ARM64, specifically the M2 Pro chip

Darwin Version vs macOS Version

Darwin versions don’t match macOS marketing versions. Here’s the mapping for recent releases:

macOS VersionMarketing NameDarwin Version
macOS 14.xSonoma23.x
macOS 13.xVentura22.x
macOS 12.xMonterey21.x
macOS 11.xBig Sur20.x
macOS 10.15Catalina19.x
macOS 10.14Mojave18.x

The pattern: Darwin version = macOS major version + 9 (approximately, with some variation).

Darwin Components in Detail

The C Library (libSystem)

Unlike Linux (which uses glibc or musl) or FreeBSD (which has its own libc), macOS uses libSystem, an umbrella library that combines:

  • Libc: Standard C library functions
  • libinfo: Directory services
  • libkvm: Kernel memory interface
  • libm: Math library
  • libpthread: POSIX threads
# Examine libSystem
$ otool -L /bin/ls | grep libSystem
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.61.1)

BSD Commands

Most command-line tools come from FreeBSD, with Apple modifications. You can verify this:

# Check version strings in common commands
$ what /bin/ls
/bin/ls:
    PROGRAM:ls  PROJECT:file_cmds-430.100.5

$ what /usr/bin/grep
/usr/bin/grep:
    PROGRAM:grep  PROJECT:text_cmds-154

The what command extracts version information embedded in binaries—a BSD convention.

Darwin’s Unique Position

Darwin occupies an interesting position in the Unix ecosystem:

  1. Certified Unix: Darwin-based macOS is UNIX 03 certified
  2. BSD heritage: Derived primarily from FreeBSD
  3. Unique kernel: XNU is unlike both monolithic Linux and pure Mach microkernels
  4. Apple stewardship: Developed primarily by Apple, with limited community input
  5. Commercial backing: Unusual for an open-source Unix

This combination means Darwin benefits from significant engineering investment while maintaining Unix compatibility. However, Apple’s control means Darwin’s direction serves Apple’s product needs, not the broader open-source community.

Working with Darwin

For practical purposes, working with Darwin means working with macOS’s command line. Throughout this book, we’ll explore Darwin’s capabilities:

  • Using BSD commands (and understanding how they differ from GNU)
  • Managing processes with Darwin’s unique tools
  • Understanding the kernel’s behavior and configuration
  • Leveraging Darwin’s networking stack
  • Working with the filesystem abstractions Darwin provides

Darwin is your gateway to macOS’s Unix power. Understanding it transforms macOS from a consumer operating system into a professional Unix workstation.

From NeXTSTEP to macOS

The story of macOS’s Unix foundation begins not at Apple, but at a company Steve Jobs founded after leaving Apple in 1985. NeXT, Inc. created an operating system so advanced that when Apple needed to modernize its aging Mac OS, it acquired the entire company to get it.

The Classic Mac OS Problem

By the mid-1990s, Apple faced a crisis. The original Macintosh operating system, while innovative for its time, lacked fundamental features that modern operating systems required:

  • No protected memory: A crash in any application could bring down the entire system
  • Cooperative multitasking: Applications had to voluntarily yield CPU time
  • No modern networking stack: TCP/IP was bolted on awkwardly
  • Limited scalability: The architecture couldn’t evolve to meet future needs

Apple attempted several internal projects to create a modern replacement—Copland, Taligent, Pink—but all failed. By 1996, Apple was searching externally for a solution.

NeXTSTEP: Unix Made Elegant

While Apple struggled, NeXT had been building something remarkable. NeXTSTEP, first released in 1989, was a Unix-based operating system with several distinctive characteristics:

Mach Microkernel Foundation

NeXTSTEP used the Mach microkernel developed at Carnegie Mellon University. Mach provided:

  • Protected memory between processes
  • Preemptive multitasking
  • Modern IPC (inter-process communication)
  • Support for multiple processor architectures

BSD Compatibility Layer

Layered atop Mach was a BSD-derived Unix environment, providing:

  • POSIX-compliant APIs
  • Standard Unix commands and utilities
  • Familiar filesystem semantics
  • TCP/IP networking

Object-Oriented Frameworks

NeXTSTEP pioneered object-oriented application development with:

  • Objective-C: An object-oriented extension to C
  • Application Kit: GUI widgets and event handling
  • Foundation: Base classes for common operations
  • Interface Builder: Visual interface design

These frameworks would evolve into today’s Cocoa.

Display PostScript

NeXTSTEP used PostScript for display rendering, ensuring that what appeared on screen matched printed output—a precursor to macOS’s Quartz.

The Acquisition

In December 1996, Apple announced it would acquire NeXT for $429 million. The deal brought:

  1. NeXTSTEP technology: The foundation for the next-generation Mac OS
  2. Steve Jobs: Who would eventually return as CEO
  3. Key engineers: Including Avie Tevanian (who became Apple’s chief software architect)

From OPENSTEP to Rhapsody to Mac OS X

The transformation from NeXTSTEP to macOS occurred in stages:

OPENSTEP (1994-1997)

Before the Apple acquisition, NeXT had separated the operating system into:

  • OPENSTEP: The application frameworks (portable to other OSes)
  • NEXTSTEP/OPENSTEP for Mach: The complete OS

This separation facilitated the eventual Apple integration.

Rhapsody (1997-1998)

Apple’s first attempt at merging NeXT technology with Mac:

  • Code-named “Rhapsody”
  • NeXTSTEP renamed and modified for Mac hardware
  • Included “Blue Box” for running classic Mac OS applications

Rhapsody was demonstrated publicly but never shipped as a consumer product.

Mac OS X Server 1.0 (1999)

The first released product combining NeXT and Apple technology:

  • Based on Rhapsody
  • Targeted at servers
  • Introduced the Aqua interface concepts
  • Still clearly NeXTSTEP under the hood

Mac OS X 10.0 (2001)

The first consumer release of the new operating system:

  • Aqua interface: New visual design replacing NeXTSTEP’s look
  • Carbon APIs: Allowing classic Mac applications to be ported
  • Cocoa APIs: The renamed OPENSTEP frameworks
  • Classic environment: Running Mac OS 9 in a compatibility layer
  • Darwin foundation: The open-source Unix base

NeXTSTEP’s Legacy in Modern macOS

Many macOS characteristics trace directly to NeXTSTEP:

File Extensions and Bundles

NeXTSTEP popularized application bundles—directories that appear as single files:

$ ls -la /Applications/Safari.app/
total 0
drwxr-xr-x   3 root  wheel    96 Dec 11 11:23 .
drwxr-xr-x  88 root  wheel  2816 Jan 15 14:20 ..
drwxr-xr-x   7 root  wheel   224 Dec 11 11:23 Contents

$ file /Applications/Safari.app/
/Applications/Safari.app/: directory

The .app extension and bundle structure come directly from NeXTSTEP.

Property Lists

The .plist files used throughout macOS for configuration:

$ file /System/Library/LaunchDaemons/com.apple.metadata.mds.plist
/System/Library/LaunchDaemons/com.apple.metadata.mds.plist: XML 1.0 document text, ASCII text

Property lists originated in NeXTSTEP as a serialization format.

The Services Menu

The Services menu (under the application menu) allowing applications to provide functionality to other applications—pure NeXTSTEP.

/Library, ~/Library, and /System/Library

The three-tier Library structure:

  • /System/Library: Apple’s system resources
  • /Library: Local administrator resources
  • ~/Library: User-specific resources

This hierarchy descends from NeXTSTEP’s organization.

launchd’s Ancestry

While launchd itself was created at Apple, its design philosophy—using property lists for service configuration—reflects NeXTSTEP’s approach to system management.

Objective-C and Cocoa

Until Swift’s introduction, Objective-C was the primary language for macOS development. Cocoa, the main application framework, is a direct evolution of OPENSTEP’s AppKit and Foundation.

The Mach Heritage

NeXTSTEP’s use of Mach brought several characteristics to macOS:

Mach Ports

Inter-process communication uses Mach ports:

$ sudo lsmp -p 1
Process (1) : launchd
  name      ipc-object    rights     flags   boost  reqs  recv  send sonce oref  qlimit  msgcount  context            identifier  type
---------   ----------    ------     -----   -----  ----  ----  ---- ----- ----  ------  --------  --------           ----------  ----
...

Memory Objects

Mach’s virtual memory system underlies macOS’s memory management:

$ vm_stat
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free:                               37103.
Pages active:                            385918.
Pages inactive:                          347108.
...

Task and Thread Model

macOS processes are Mach tasks containing threads—terminology that persists in debugging and profiling tools.

Understanding the Timeline

Here’s a consolidated timeline:

YearEvent
1985Steve Jobs leaves Apple, founds NeXT
1988NeXTSTEP 0.8 released
1989NeXTSTEP 1.0 released
1993NeXTSTEP 3.0 - mature, stable release
1994OPENSTEP specification published
1996Apple acquires NeXT
1997Steve Jobs returns to Apple
1999Mac OS X Server 1.0 released
2000Darwin open-sourced
2001Mac OS X 10.0 “Cheetah” released
2012OS X 10.8 “Mountain Lion” - “Mac” dropped from name
2016macOS 10.12 “Sierra” - rebranded to “macOS”
2020macOS 11 “Big Sur” - first version for Apple Silicon

Why This History Matters

Understanding NeXTSTEP’s influence helps explain macOS peculiarities:

  1. Why Objective-C? Because NeXTSTEP chose it in the 1980s
  2. Why property lists? NeXTSTEP’s serialization format
  3. Why application bundles? NeXTSTEP’s packaging model
  4. Why Mach ports? NeXTSTEP’s kernel choice
  5. Why is Darwin BSD-based? NeXTSTEP’s Unix compatibility layer

When you encounter something in macOS that seems different from Linux or traditional BSD, the answer often lies in this NeXTSTEP heritage. The next chapter examines the kernel architecture that emerged from this history.

The XNU Kernel Architecture

XNU—“X is Not Unix” (a recursive acronym in the tradition of GNU)—is the kernel at the heart of Darwin. XNU is neither a traditional monolithic kernel like Linux nor a pure microkernel like Mach was intended to be. It’s a hybrid that combines elements of both approaches.

What Is a Kernel?

Before diving into XNU’s specifics, let’s clarify what a kernel does:

  • Process management: Creating, scheduling, and terminating processes
  • Memory management: Allocating RAM, implementing virtual memory
  • Device drivers: Interfacing with hardware
  • System calls: Providing services to user-space programs
  • IPC: Facilitating communication between processes

How these responsibilities are organized defines a kernel’s architecture.

Kernel Architecture Styles

Monolithic Kernels

Linux and traditional BSD use monolithic kernels:

  • All kernel services run in kernel space
  • Efficient: no context switches between kernel components
  • Risk: a bug in any driver can crash the entire system

Microkernels

Pure microkernels (like MINIX or L4) take a different approach:

  • Minimal kernel: only scheduling, IPC, basic memory management
  • Services (filesystems, drivers) run as user-space processes
  • Robust: service crashes don’t bring down the kernel
  • Overhead: IPC between services adds latency

XNU: The Hybrid Approach

XNU combines both:

  • Mach layer: Microkernel-derived component handling IPC, virtual memory, scheduling
  • BSD layer: Monolithic component providing Unix APIs, networking, filesystems
  • IOKit: Object-oriented driver framework in kernel space
┌─────────────────────────────────────────────────────────┐
│                    User Space                            │
│  Applications, Daemons, Shell commands                   │
├─────────────────────────────────────────────────────────┤
│                    System Calls                          │
├───────────────┬────────────────────────┬────────────────┤
│   BSD Layer   │      IOKit/Drivers     │                │
│  (POSIX APIs, │    (Device drivers,    │                │
│   networking, │   hardware abstraction)│                │
│  filesystems) │                        │                │
├───────────────┴────────────────────────┴────────────────┤
│                    Mach Layer                            │
│  (IPC, virtual memory, scheduling, timers)               │
├─────────────────────────────────────────────────────────┤
│                    Hardware                              │
└─────────────────────────────────────────────────────────┘

The Mach Layer

XNU’s Mach component derives from Mach 3.0, developed at Carnegie Mellon University. However, XNU doesn’t use Mach as a true microkernel—the BSD layer runs in kernel space, not as a user-space server.

Mach Abstractions

Mach provides several fundamental abstractions:

Tasks: The unit of resource ownership. A task contains threads and owns resources like memory and ports. In Unix terms, a task roughly corresponds to a process.

# View Mach task information
$ sudo launchctl procinfo 1
{
    "task_info" : {
        "suspend_count" : 0,
        "virtual_size" : 418099200,
        "resident_size" : 5423104,
        ...
    }
}

Threads: The unit of execution. Tasks contain one or more threads.

Ports: The fundamental IPC mechanism. Ports are endpoints for message passing.

# List Mach ports for a process
$ sudo lsmp -p $$

Messages: Data passed between ports. All Mach IPC occurs through message passing.

Memory Objects: Abstractions for memory that can be mapped into task address spaces.

Mach IPC

Mach’s message-passing IPC is powerful but complex:

// Simplified Mach message send
mach_msg_header_t msg;
msg.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
msg.msgh_size = sizeof(msg);
msg.msgh_remote_port = destination_port;
msg.msgh_local_port = MACH_PORT_NULL;
mach_msg(&msg, MACH_SEND_MSG, sizeof(msg), 0,
         MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

While application developers rarely use Mach IPC directly (preferring higher-level APIs like XPC), it underpins many macOS facilities.

Virtual Memory

Mach’s virtual memory system provides:

  • Sparse address spaces: Processes can have large virtual address spaces
  • Copy-on-write: Efficient process forking and memory sharing
  • Memory-mapped files: Files accessible as memory regions
  • External pagers: Pluggable systems for backing memory with storage
# Examine Mach VM statistics
$ vm_stat
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free:                               32148.
Pages active:                            402844.
Pages inactive:                          373521.
Pages speculative:                        11276.
Pages throttled:                              0.
Pages wired down:                         96482.
Pages purgeable:                          23159.
"Translation faults":                 135941690.
Pages copy-on-write:                    4972318.
Pages zero filled:                     62938502.
Pages reactivated:                      4058508.
Pages purged:                            789235.
...

Scheduling

Mach handles thread scheduling with multiple scheduling policies:

  • Timeshare: Standard interactive scheduling
  • Fixed priority: Real-time scheduling with fixed priorities
  • Round-robin: Equal time slices
# View thread scheduling information
$ sudo spindump -noProcesses 1 -stdout | head -50

The BSD Layer

The BSD layer provides the Unix personality that applications see:

POSIX APIs

Standard Unix system calls are implemented in the BSD layer:

  • fork(), exec(), wait()
  • open(), read(), write(), close()
  • socket(), bind(), listen(), accept()
  • kill(), signal handling

Process Model

The BSD layer maintains the traditional Unix process model atop Mach tasks:

# BSD process information
$ ps aux | head -5
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root     1   0.0  0.1 410045680  18512   ??  Ss   Tue10AM   3:21.67 /sbin/launchd

# Underlying Mach task information
$ sudo taskinfo 1

Each BSD process corresponds to a Mach task, but the BSD layer provides the familiar Unix semantics.

Networking Stack

The BSD layer includes a complete TCP/IP networking stack derived from FreeBSD:

# View network statistics (BSD-style)
$ netstat -s | head -30
tcp:
    14548388 packets sent
        9522841 data packets (5765485687 bytes)
        96487 data packets (87239347 bytes) retransmitted
        0 resend initiated by MTU discovery
        3896370 ack-only packets (2451961 delayed)
...

Filesystems

Filesystem implementations live in the BSD layer:

  • APFS: Apple File System (modern default)
  • HFS+: Hierarchical File System Plus (legacy)
  • NFS: Network File System
  • SMB: Server Message Block
  • devfs: Device filesystem
  • Various FUSE filesystems: Via macFUSE

The VFS Layer

The Virtual File System layer abstracts filesystem differences:

# List mounted filesystems
$ mount
/dev/disk3s1s1 on / (apfs, sealed, local, read-only, journaled)
devfs on /dev (devfs, local, nobrowse)
/dev/disk3s6 on /System/Volumes/VM (apfs, local, noexec, journaled, noatime, nobrowse)
/dev/disk3s2 on /System/Volumes/Preboot (apfs, local, journaled, nobrowse)
/dev/disk3s4 on /System/Volumes/Update (apfs, local, journaled, nobrowse)
/dev/disk3s5 on /System/Volumes/Data (apfs, local, journaled, nobrowse, protect)

IOKit: The Driver Framework

IOKit is XNU’s object-oriented framework for device drivers:

Design Philosophy

  • Written in a restricted subset of C++ (Embedded C++)
  • Object-oriented: drivers inherit from base classes
  • Uses a registry to match drivers with devices
  • Power management integrated at the framework level

The IORegistry

IOKit maintains a database of devices and their relationships:

# View the IORegistry
$ ioreg -l | head -50
+-o Root  <class IORegistryEntry, id 0x100000100, retain 36>
  +-o MacBookPro18,3  <class IOPlatformExpertDevice, id 0x100000116, registered, matched, active, busy 0 (10112 ms), retain 61>
    | {
    |   "IOBusyInterest" = "IOCommand is not serializable"
    |   "IOPlatformSerialNumber" = "..."
    |   "IOPolledInterface" = "SMCPolledInterface is not serializable"
    |   "IOPlatformUUID" = "..."
...

# List USB devices through IOKit
$ ioreg -p IOUSB -l -w0

# List storage devices
$ ioreg -c IOMedia -l

Kernel Extensions (Kexts)

Traditionally, third-party drivers were loaded as kernel extensions:

# List loaded kexts
$ kextstat | head -10
Index Refs Address            Size       Wired      Name (Version) UUID <Linked Against>
    1  169 0                  0          0          com.apple.kpi.bsd (23.4.0)
    2   18 0                  0          0          com.apple.kpi.dsep (23.4.0)
    3  196 0                  0          0          com.apple.kpi.iokit (23.4.0)
...

However, modern macOS strongly discourages kexts in favor of:

  • System Extensions: User-space drivers for many device types
  • DriverKit: A framework for building drivers that run outside the kernel

This shift improves security and stability by moving driver code out of kernel space.

System Calls in XNU

XNU provides system calls through multiple interfaces:

Mach Traps

Low-level Mach operations:

// Example Mach trap numbers
#define MACH_MSG_TRAP         -31
#define MACH_REPLY_PORT       -26
#define THREAD_SELF_TRAP      -27
#define TASK_SELF_TRAP        -28

BSD System Calls

Standard Unix system calls:

# View system call numbers
$ grep -E "^#define\s+SYS_" /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/syscall.h | head -20
#define SYS_syscall        0
#define SYS_exit           1
#define SYS_fork           2
#define SYS_read           3
#define SYS_write          4
#define SYS_open           5
#define SYS_close          6
#define SYS_wait4          7
...

Machine-Dependent Calls

Architecture-specific operations.

Kernel Debugging and Introspection

Several tools expose XNU internals:

sysctl

The sysctl interface queries and sets kernel parameters:

# View kernel information
$ sysctl kern.ostype kern.osrelease kern.hostname
kern.ostype: Darwin
kern.osrelease: 23.4.0
kern.hostname: MacBook-Pro.local

# List all kernel parameters
$ sysctl -a | wc -l
    1847

# View memory parameters
$ sysctl vm
vm.loadavg: { 1.83 2.07 2.15 }
vm.swapusage: total = 0.00M  used = 0.00M  free = 0.00M  (encrypted)
vm.cs_validation: 1
...

DTrace (Where Available)

On systems where it’s available, DTrace provides deep kernel tracing:

# Trace system calls (requires SIP adjustment)
$ sudo dtrace -n 'syscall:::entry { @[execname] = count(); }'

Note: System Integrity Protection restricts DTrace on modern macOS.

Instruments

Apple’s Instruments application provides profiling using kernel trace facilities:

# Command-line profiling
$ xctrace record --template 'Time Profiler' --launch -- /path/to/app

XNU Source Code Structure

For those interested in exploring the code:

xnu/
├── bsd/           # BSD layer (processes, filesystems, networking)
├── iokit/         # IOKit driver framework
├── libkern/       # Kernel C++ runtime
├── libsa/         # Standalone library
├── osfmk/         # Mach layer (scheduling, IPC, VM)
├── pexpert/       # Platform expert (hardware abstraction)
└── security/      # Security policies (MAC framework)

The source is available at opensource.apple.com, though building it requires significant effort.

Why XNU’s Architecture Matters

Understanding XNU helps explain several macOS behaviors:

  1. Why Mach-O binaries? XNU uses Mach-O format, not ELF like Linux
  2. Why different process tools? BSD and Mach layers provide overlapping but different views
  3. Why complex memory semantics? Mach’s VM model has unique characteristics
  4. Why DriverKit? Moving away from kernel extensions reflects security priorities

The hybrid nature of XNU—combining Mach’s IPC and memory management with BSD’s Unix compatibility—creates a unique system that’s familiar yet different from both Linux and traditional BSD.

macOS vs Linux vs BSD: Key Differences

If you’re coming to macOS from Linux or BSD, many things will feel familiar—but the differences can be surprising and sometimes frustrating. This chapter catalogs the key differences you’ll encounter, explaining not just what differs but why.

Kernel and System Architecture

Kernel Type

SystemKernelType
macOSXNUHybrid (Mach + BSD)
LinuxLinuxMonolithic (with modules)
FreeBSDFreeBSDMonolithic (with modules)
OpenBSDOpenBSDMonolithic

Impact: macOS uses Mach-O binary format instead of ELF. Tools that work with binaries (nm, objdump, ldd) have different equivalents on macOS.

# Linux: View shared libraries
$ ldd /bin/ls

# macOS: View shared libraries (different tool)
$ otool -L /bin/ls
/bin/ls:
    /usr/lib/libutil.dylib (compatibility version 1.0.0, current version 1.0.0)
    /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.61.1)

Init System

SystemInit SystemConfig Format
macOSlaunchdProperty lists (XML/binary)
Linux (modern)systemdUnit files (INI-like)
Linux (traditional)SysVinitShell scripts
FreeBSDrc.dShell scripts

Impact: Service management is completely different on macOS. There’s no systemctl, no /etc/init.d/, no chkconfig.

# Linux (systemd)
$ sudo systemctl start nginx
$ sudo systemctl enable nginx

# macOS (launchd)
$ sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.nginx.plist
$ sudo launchctl enable system/homebrew.mxcl.nginx

Command-Line Tools

BSD vs GNU Commands

macOS ships with BSD versions of common commands. These often have different options than GNU versions:

sed

# GNU sed (Linux): In-place edit
$ sed -i 's/old/new/g' file.txt

# BSD sed (macOS): Requires backup extension (use '' for no backup)
$ sed -i '' 's/old/new/g' file.txt

grep

# GNU grep: Perl regex with -P
$ grep -P '\d{3}-\d{4}' file.txt

# BSD grep (macOS): No -P option, use -E for extended regex
$ grep -E '[0-9]{3}-[0-9]{4}' file.txt

ls

# GNU ls: Color output with --color
$ ls --color=auto

# BSD ls (macOS): Color with -G
$ ls -G

# GNU ls: Human-readable with -h requires -l
$ ls -lh

# BSD ls: -h works without -l (but usually used with -l anyway)
$ ls -lh

date

# GNU date: Specific date formatting
$ date -d "2024-01-15" +%s

# BSD date (macOS): Different syntax
$ date -j -f "%Y-%m-%d" "2024-01-15" +%s

stat

# GNU stat
$ stat --format="%s" file.txt

# BSD stat (macOS)
$ stat -f "%z" file.txt
# GNU readlink: -f to canonicalize
$ readlink -f /path/to/symlink

# BSD readlink (macOS): No -f, but realpath exists (macOS 12.3+)
$ realpath /path/to/symlink
# Or for older macOS:
$ python3 -c "import os; print(os.path.realpath('/path/to/symlink'))"

macOS-Specific Commands

macOS includes commands with no Linux/BSD equivalent:

# Copy to clipboard
$ echo "Hello" | pbcopy
$ pbpaste

# Open files with default application
$ open file.pdf
$ open -a Safari https://apple.com
$ open .  # Open current directory in Finder

# Spotlight search
$ mdfind "search term"
$ mdfind -name "filename"
$ mdfind "kMDItemContentType == 'public.jpeg'"

# Text-to-speech
$ say "Hello, world"

# Manage system preferences
$ open "x-apple.systempreferences:"

# Screen capture
$ screencapture -i screenshot.png

# Airport (Wi-Fi) utility
$ /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s

# Disk utility
$ diskutil list
$ diskutil info disk0

Network Commands

Networking tools differ significantly:

# Linux: Modern networking
$ ip addr show
$ ip route
$ ss -tuln

# macOS: BSD networking
$ ifconfig
$ netstat -rn
$ netstat -an | grep LISTEN

Note: ip doesn’t exist on macOS (unless installed via Homebrew). macOS uses traditional BSD tools like ifconfig, netstat, and route.

# Linux: Network manager
$ nmcli device status

# macOS: networksetup
$ networksetup -listallhardwareports
$ networksetup -getinfo "Wi-Fi"

Filesystem Differences

Filesystem Types

SystemDefault FSFeatures
macOSAPFSSnapshots, encryption, space sharing, clones
Linuxext4 / XFS / BtrfsVaries by filesystem
FreeBSDZFS / UFSZFS has similar features to APFS

Case Sensitivity

macOS is case-insensitive but case-preserving by default:

# On default macOS filesystem:
$ touch File.txt
$ ls file.txt
File.txt           # Finds it! Different from Linux

This can cause issues with Git repositories from case-sensitive systems.

Filesystem Hierarchy

Key differences from the Filesystem Hierarchy Standard (FHS):

PurposeLinux/BSD FHSmacOS
Applications/usr/bin, /opt/Applications
System config/etc/etc (Unix) + /Library/Preferences (macOS)
User config~/.config~/Library
User dataVarious~/Documents, ~/Library/Application Support
Temp files/tmp/tmp → /private/tmp
Variable data/var/var → /private/var
System libraries/usr/lib/usr/lib + /System/Library/Frameworks
Third-party/opt, /usr/local/usr/local, /opt/homebrew (ARM)

Extended Attributes

macOS uses extended attributes heavily:

# View extended attributes
$ xattr file.txt
com.apple.quarantine

# List with values
$ xattr -l file.txt
com.apple.quarantine: 0083;5f4a7c14;Safari;...

# Remove quarantine attribute
$ xattr -d com.apple.quarantine file.txt

# Clear all attributes
$ xattr -c file.txt

Linux has extended attributes too, but they’re less pervasive:

# Linux extended attributes
$ getfattr -d file.txt
$ setfattr -n user.comment -v "test" file.txt

Resource Forks and Metadata

macOS preserves metadata when copying files:

# macOS: Copy with metadata
$ cp -p file.txt dest/          # Preserves basic metadata
$ ditto source/ dest/           # Preserves everything including resource forks

# Problem: copying to non-HFS+/APFS filesystems
$ cp file.txt /Volumes/USB_FAT32/
# Creates file.txt and ._file.txt (AppleDouble file)

Those ._* files you see on USB drives? That’s macOS preserving metadata on filesystems that don’t support it natively.

Package Management

No Native Package Manager

Unlike most Linux distributions and BSD systems, macOS doesn’t include a built-in package manager for third-party software:

SystemNative Package Manager
macOSNone (uses App Store for GUI apps)
Debian/Ubuntuapt
Red Hat/Fedoradnf/yum
Archpacman
FreeBSDpkg
OpenBSDpkg_add

Solutions for macOS:

# Homebrew (most popular)
$ brew install wget

# MacPorts (alternative)
$ sudo port install wget

User and Permission Differences

User IDs

UserLinuxmacOS
rootUID 0UID 0
First userUID 1000UID 501
NobodyUID 65534UID -2

Groups

macOS has different default groups:

$ id
uid=501(david) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),...

The primary group for users is staff (GID 20), not a user-specific group.

System Integrity Protection (SIP)

macOS restricts what even root can do:

# This fails even as root (SIP protected):
$ sudo rm /System/Library/CoreServices/Finder.app
rm: /System/Library/CoreServices/Finder.app: Operation not permitted

# Check SIP status
$ csrutil status
System Integrity Protection status: enabled.

Linux and BSD have no equivalent to SIP—root can do anything.

Sudo Configuration

macOS doesn’t use /etc/sudoers by default for most permissions. Instead, it uses admin group membership:

# Check who can sudo
$ grep -E "^%admin|^%wheel" /etc/sudoers
%admin		ALL = (ALL) ALL

Process and System Management

Process Listing

# Both work on macOS:
$ ps aux                    # BSD syntax
$ ps -ef                    # POSIX syntax

# Linux-specific options don't work:
$ ps --forest              # Error on macOS

System Information

# Linux
$ cat /proc/cpuinfo
$ free -h
$ lsb_release -a

# macOS (no /proc filesystem)
$ sysctl -n machdep.cpu.brand_string
$ vm_stat                   # Memory (in pages, not bytes)
$ sw_vers                   # macOS version

Signals

Signal numbers can differ:

# Check signal numbers
$ kill -l

Most common signals (SIGTERM=15, SIGKILL=9, SIGHUP=1) are the same.

Development Environment

Compiler

# Linux (usually GCC)
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0

# macOS (Clang masquerading as gcc)
$ gcc --version
Apple clang version 15.0.0 (clang-1500.3.9.4)

The gcc command on macOS is actually Clang. This usually doesn’t matter, but some software with GCC-specific features may need adjustment.

Libraries

# Linux: Shared library extension
libfoo.so, libfoo.so.1, libfoo.so.1.0.0

# macOS: Shared library extension
libfoo.dylib, libfoo.1.dylib, libfoo.1.0.0.dylib

# Linux: Find library
$ ldconfig -p | grep libssl
$ ldd /usr/bin/openssl

# macOS: Find library
$ otool -L /usr/bin/openssl

Library Paths

# Linux
$ echo $LD_LIBRARY_PATH
$ cat /etc/ld.so.conf

# macOS
$ echo $DYLD_LIBRARY_PATH           # Runtime
$ echo $DYLD_FALLBACK_LIBRARY_PATH  # Fallback

Virtualization and Containers

Docker

# Linux: Docker runs natively
$ docker run alpine echo "Hello"

# macOS: Docker runs in a Linux VM
$ docker run alpine echo "Hello"
# (Actually runs in a hypervisor-backed Linux VM)

Docker Desktop on macOS uses a Linux virtual machine because containers are a Linux kernel feature.

Native Virtualization

# macOS: Virtualization.framework (Apple Silicon) or Hypervisor.framework
# No direct CLI, used by apps like UTM, Parallels, Docker Desktop

# Linux: KVM
$ virsh list --all

Summary Table

FeaturemacOSLinuxFreeBSD
KernelXNU (hybrid)Linux (monolithic)FreeBSD (monolithic)
Initlaunchdsystemd (usually)rc.d
Package mgrHomebrew (3rd party)apt/dnf/pacmanpkg
Shell defaultzshbash (usually)sh/tcsh
FilesystemAPFSext4/XFS/BtrfsZFS/UFS
Case sensitiveNo (default)YesConfigurable
Binary formatMach-OELFELF
CommandsBSDGNUBSD
Firewallpfiptables/nftablespf/ipfw
Root restrictionSIPSELinux/AppArmorsecurelevel

Understanding these differences helps you translate your Unix knowledge to macOS effectively. When something doesn’t work as expected, check whether it’s a BSD vs GNU difference, a filesystem assumption, or a macOS-specific security feature.

POSIX Compliance and Standards

When discussing Unix compatibility, you’ll often hear about “POSIX compliance.” macOS is notable for being both POSIX-compliant and UNIX-certified—distinctions that matter for software portability and system behavior.

What Is POSIX?

POSIX (Portable Operating System Interface) is a family of standards specified by the IEEE Computer Society for maintaining compatibility between operating systems. POSIX defines:

  • System interfaces: C API functions like open(), fork(), pthread_create()
  • Shell and utilities: Command-line tools and their expected behavior
  • Shell language: The syntax and features of POSIX shell scripts
  • Threads: The pthreads API for multi-threaded programming
  • Real-time extensions: APIs for real-time computing

The goal: write code once, run it on any POSIX-compliant system.

POSIX vs UNIX Certification

These are related but different:

POSIX Compliance

A system that implements POSIX interfaces. Self-declared, no formal certification required.

  • Linux: POSIX-compliant (mostly)
  • FreeBSD: POSIX-compliant
  • macOS: POSIX-compliant and certified

UNIX Certification

A formal certification from The Open Group, which owns the UNIX trademark. Requires:

  1. Passing conformance tests
  2. Paying certification fees
  3. Regular re-certification

Currently certified UNIX systems:

  • macOS (UNIX 03)
  • IBM AIX
  • HP-UX
  • Oracle Solaris

Notable non-certified systems:

  • Linux (too expensive/complex to certify each distribution)
  • FreeBSD (philosophically opposed to the process)

What Certification Means

macOS’s UNIX 03 certification means it has passed The Open Group’s test suite demonstrating compliance with:

  • Single UNIX Specification version 3 (SUS v3)
  • POSIX.1-2001
  • Related standards

You can verify this:

# Check POSIX version supported
$ getconf _POSIX_VERSION
200112

# Check UNIX version (SUS)
$ getconf _XOPEN_VERSION
600

POSIX on macOS in Practice

Standard Headers

macOS provides all required POSIX headers:

$ ls /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/ | head
_endian.h
_posix_availability.h
_pthread
_select.h
_structs.h
_symbol_aliasing.h
_types
_types.h
acct.h
acl.h

Feature Test Macros

To access POSIX-specific features in C code:

#define _POSIX_C_SOURCE 200112L  // Request POSIX.1-2001 features
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

Or for all UNIX features:

#define _XOPEN_SOURCE 600  // Request SUS v3 features
#include <stdlib.h>

Checking POSIX Limits

POSIX defines various system limits that can be queried:

# Maximum arguments to exec()
$ getconf ARG_MAX
1048576

# Maximum filename length
$ getconf NAME_MAX /
255

# Maximum path length
$ getconf PATH_MAX /
1024

# Number of processors
$ getconf _NPROCESSORS_ONLN
10

# POSIX version
$ getconf _POSIX_VERSION
200112

In C code:

#include <unistd.h>
#include <limits.h>

long arg_max = sysconf(_SC_ARG_MAX);
long open_max = sysconf(_SC_OPEN_MAX);
long nprocessors = sysconf(_SC_NPROCESSORS_ONLN);

POSIX Shell Scripting

POSIX defines a portable shell language. Scripts targeting POSIX should:

Use /bin/sh

#!/bin/sh
# Not #!/bin/bash or #!/bin/zsh

On macOS, /bin/sh is actually bash running in POSIX mode (or zsh on newer versions in some contexts):

$ file /bin/sh
/bin/sh: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]

$ /bin/sh --version
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)

Avoid Bashisms

Common bash features that aren’t POSIX:

# Non-POSIX (bash-specific)
[[ $var == "test" ]]     # Double brackets
array=(one two three)     # Arrays
${var//old/new}          # Pattern substitution
$((var++))               # C-style increment
source file.sh           # source command

# POSIX equivalents
[ "$var" = "test" ]      # Single brackets, single =
# No arrays in POSIX
# Use sed for substitution
var=$((var + 1))         # POSIX arithmetic
. file.sh                # Dot command

Test Your Scripts

Check scripts for POSIX compliance:

# Install shellcheck
$ brew install shellcheck

# Check a script
$ shellcheck --shell=sh myscript.sh

Shebang Paths

POSIX doesn’t specify interpreter locations:

#!/bin/bash              # Works on most Linux, macOS
#!/usr/bin/env bash      # More portable - finds bash in PATH

On some systems, bash might be in /usr/local/bin/bash or elsewhere.

Command Options

Even when commands exist, options may differ:

# POSIX requires these options for 'ls'
# -a, -A, -C, -d, -F, -g, -i, -l, -n, -o, -p, -q, -r, -R, -s, -t, -u, -1

# These are NOT required by POSIX:
ls --color     # GNU extension
ls -G          # BSD extension (macOS)

printf vs echo

echo behavior varies between systems. POSIX recommends printf:

# Portable
printf '%s\n' "Hello"
printf 'Value: %d\n' 42

# Not portable (echo -n, echo -e behavior varies)
echo -n "No newline"      # Not POSIX
echo -e "Tab:\tHere"     # Not POSIX

Regular Expressions

POSIX defines Basic Regular Expressions (BRE) and Extended Regular Expressions (ERE):

# BRE (default for grep, sed)
grep 'a\+b'              # + must be escaped
grep 'a\{2,3\}'          # Braces must be escaped

# ERE (grep -E, egrep, awk)
grep -E 'a+b'            # + doesn't need escaping
grep -E 'a{2,3}'         # Braces don't need escaping

Perl-compatible regex (PCRE) is NOT part of POSIX:

grep -P '\d+'            # GNU extension - NOT on macOS
grep -E '[0-9]+'         # POSIX ERE equivalent

Extensions Beyond POSIX

macOS includes extensions beyond POSIX requirements:

BSD Extensions

# sysctl - BSD system control
$ sysctl kern.hostname

# BSD-style ps flags
$ ps aux

# Disk management
$ diskutil list

Apple Extensions

# Spotlight search
$ mdfind "query"

# Clipboard
$ pbcopy < file.txt

# Open files
$ open document.pdf

# System configuration
$ defaults read com.apple.finder

These extensions are not portable to Linux or other systems.

Writing Portable Code

For Shell Scripts

  1. Use #!/bin/sh and stick to POSIX features
  2. Test with shellcheck --shell=sh
  3. Avoid command options not listed in POSIX
  4. Use printf instead of echo for complex output
  5. Use [ ] instead of [[ ]]
#!/bin/sh
# Portable script example

set -e  # Exit on error (POSIX)

# Check if file exists
if [ -f "$1" ]; then
    printf 'Processing: %s\n' "$1"
    # Use cat (POSIX) not bashisms
    cat "$1" | while IFS= read -r line; do
        printf '%s\n' "$line"
    done
else
    printf 'File not found: %s\n' "$1" >&2
    exit 1
fi

For C Programs

  1. Include _POSIX_C_SOURCE or _XOPEN_SOURCE appropriately
  2. Check return values (POSIX mandates specific error codes)
  3. Use configure scripts to detect platform differences
  4. Provide fallbacks for non-POSIX features
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main(void) {
    char *cwd = getcwd(NULL, 0);  // POSIX.1-2001
    if (cwd == NULL) {
        perror("getcwd");
        return EXIT_FAILURE;
    }
    printf("Current directory: %s\n", cwd);
    free(cwd);
    return EXIT_SUCCESS;
}

Autoconf and Portable Building

For larger projects, GNU Autotools help detect platform differences:

# Generate configure script
$ autoreconf -i

# Configure for the current platform
$ ./configure

# Build
$ make

The configure script detects macOS vs Linux differences and sets appropriate compiler flags.

Checking Standards Compliance

On macOS

# Compiler standards mode
$ cc -std=c11 -D_POSIX_C_SOURCE=200809L -Wall -pedantic program.c

# Check what POSIX version the system claims
$ getconf POSIX_VERSION
200112

# Check POSIX limits
$ getconf -a | grep POSIX

Useful References

Summary

macOS’s POSIX compliance and UNIX certification mean:

  1. Standard APIs work: POSIX C functions behave correctly
  2. Basic commands exist: Core utilities are available
  3. Shell scripts can be portable: With discipline
  4. Not everything is standard: macOS and Apple add extensions

When writing portable code:

  • Stick to POSIX-defined features when possible
  • Test on multiple platforms
  • Use tools like shellcheck and autoconf
  • Be aware that BSD and GNU versions of tools differ

The standards provide a foundation, but real-world portability requires attention to detail and testing across target systems.

Understanding macOS Filesystems

Filesystems are where Unix heritage meets Apple innovation. While macOS supports familiar Unix filesystem concepts—files, directories, permissions, symbolic links—it implements them atop storage technologies that differ significantly from traditional Unix systems.

Understanding macOS filesystems means understanding three layers:

  1. APFS: The modern storage technology providing advanced features like snapshots, clones, and space sharing
  2. The VFS layer: How macOS presents a unified filesystem interface to applications
  3. macOS conventions: Extended attributes, metadata, and organizational patterns unique to Mac

The Evolution of Mac Storage

The journey from original Macintosh to modern macOS represents a dramatic evolution:

1984-1998: The original Macintosh File System (MFS) and its successor HFS

  • Resource forks and data forks as first-class concepts
  • Case-insensitive by design
  • No Unix permissions (Mac OS had no concept of users)

1998-2017: HFS+ (Mac OS Extended)

  • Journaling for data integrity
  • Support for Unix permissions (added for Mac OS X)
  • Still case-insensitive by default
  • Resource forks maintained for compatibility

2017-Present: APFS (Apple File System)

  • Built for flash storage and SSDs
  • Native encryption
  • Snapshots and cloning
  • Space sharing between volumes
  • Case-sensitive option more practical

What You’ll Learn in This Part

APFS: Apple’s Modern Filesystem explains the architecture and capabilities of Apple’s current filesystem, including features like space sharing, snapshots, and encryption.

HFS+ Legacy and Migration covers the previous filesystem format, why you might still encounter it, and how to handle legacy volumes.

Case Sensitivity: Options and Implications addresses one of the most misunderstood aspects of macOS filesystems—why case-insensitivity is the default and when it matters.

Extended Attributes and Resource Forks explores macOS’s rich file metadata system, including quarantine attributes, Finder information, and the legacy of resource forks.

The Metadata Files: .DS_Store and ._AppleDouble explains those mysterious files that appear everywhere and how to manage them.

Filesystem Hierarchy: Where macOS Diverges maps out where things live on macOS compared to the Filesystem Hierarchy Standard used by Linux.

Disk Management from the Command Line teaches you to manage disks, partitions, and volumes using Terminal commands.

Volumes, Containers, and Snapshots dives deep into APFS’s container architecture and how to work with snapshots.

Why Filesystems Matter

As a Unix user, you might assume filesystems are interchangeable—files are files, directories are directories. On macOS, this assumption can lead to problems:

  • Copy a file to a FAT32 USB drive and mysterious ._ files appear
  • Git reports changes to files you didn’t modify (case sensitivity issues)
  • Scripts fail because they assume /opt exists or /tmp persists across reboots
  • Files downloaded from the internet won’t run (quarantine attributes)

Understanding macOS filesystems helps you avoid these pitfalls and leverage features like snapshots and clones that traditional Unix filesystems lack.

APFS: Apple’s Modern Filesystem

Apple File System (APFS) replaced HFS+ as macOS’s default filesystem in 2017. Designed for flash storage and modern security requirements, APFS brings capabilities that Unix veterans may find familiar from ZFS or Btrfs—but with Apple’s distinctive implementation choices.

Design Goals

APFS was designed to address HFS+’s limitations:

  • Flash optimization: HFS+ was designed for spinning disks; APFS is optimized for SSDs
  • Crash protection: Copy-on-write metadata ensures consistency without full-volume journaling
  • Encryption: Native encryption integrated at the filesystem level
  • Space efficiency: Space sharing eliminates wasted space from traditional partitioning
  • Snapshots: Point-in-time filesystem states for backups and Time Machine
  • Clones: Instant file copies that share underlying data

Core Concepts

Containers and Volumes

APFS introduces a two-level hierarchy that differs from traditional partitioning:

┌─────────────────────────────────────────────────┐
│                Physical Disk                     │
├─────────────────────────────────────────────────┤
│              APFS Container                      │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐           │
│  │ Volume  │ │ Volume  │ │ Volume  │           │
│  │ (macOS) │ │ (Data)  │ │ (Preboot│           │
│  │         │ │         │ │         │           │
│  └─────────┘ └─────────┘ └─────────┘           │
│                                                  │
│       All volumes share container space          │
└─────────────────────────────────────────────────┘

Container: The APFS equivalent of a partition. A container occupies a contiguous region of the disk and can hold multiple volumes.

Volume: A logical filesystem within a container. Multiple volumes share the container’s space dynamically—no need to pre-allocate sizes.

View your system’s structure:

$ diskutil list
/dev/disk0 (internal):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *500.1 GB   disk0
   1:             Apple_APFS_ISC Container disk1         524.3 MB   disk0s1
   2:                 Apple_APFS Container disk3         494.4 GB   disk0s2
   3:        Apple_APFS_Recovery Container disk2         5.4 GB     disk0s3

/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +494.4 GB   disk3
   1:                APFS Volume Macintosh HD            9.8 GB     disk3s1
   2:              APFS Snapshot com.apple.os.update-... 9.8 GB     disk3s1s1
   3:                APFS Volume Preboot                 5.3 GB     disk3s2
   4:                APFS Volume Recovery                1.6 GB     disk3s3
   5:                APFS Volume Data                    256.3 GB   disk3s5
   6:                APFS Volume VM                      3.2 GB     disk3s6

Space Sharing

Unlike traditional partitions with fixed sizes, APFS volumes share space:

# View container space usage
$ diskutil apfs list
...
+-- Container disk3 494.4 GB
    ====================================================
    APFS Container Reference:     disk3
    Size (Capacity Ceiling):      494384795648 B (494.4 GB)
    Capacity In Use By Volumes:   275847008256 B (275.8 GB) (55.8%)
    Capacity Not Allocated:       218537787392 B (218.5 GB) (44.2%)
    |
    +-< Physical Store disk0s2 494.4 GB
    |
    +-> Volume disk3s1 Macintosh HD
    |   ---------------------------------------------------
    |   APFS Volume Disk (Role):   disk3s1 (System)
    |   Name:                      Macintosh HD
    |   Mount Point:               /
    |   Capacity Consumed:         9.8 GB
    ...

The “Capacity Not Allocated” is available to any volume in the container—no repartitioning needed.

Volume Roles

APFS assigns roles to volumes that affect their behavior:

RolePurposeMount Point
SystemBoot volume, read-only/
DataUser data/System/Volumes/Data (firmlinked to appear at /Users, /Library, etc.)
VMSwap and sleep image/System/Volumes/VM
PrebootBoot helper data/System/Volumes/Preboot
RecoveryRecovery OSNot normally mounted
# View volume roles
$ diskutil apfs listVolumes disk3
...
Role:              Data
Role:              System
Role:              Preboot
Role:              Recovery
Role:              VM

Key Features

Copy-on-Write

APFS uses copy-on-write (CoW) for metadata and optionally for data:

  • Metadata: Always CoW; changes write to new locations, then atomically update references
  • Data: CoW when cloning files; regular writes may overwrite in place

This provides crash protection without the overhead of journaling every write.

Clones: Instant File Copies

When you copy a file on APFS, the filesystem can create a clone:

# Create a clone (instant, regardless of file size)
$ cp -c largefile.iso largefile_copy.iso

# Both files share the same data blocks
$ ls -ls largefile.iso largefile_copy.iso
0 -rw-r--r--  1 user  staff  4700000000 Jan 15 10:30 largefile.iso
0 -rw-r--r--  1 user  staff  4700000000 Jan 15 10:30 largefile_copy.iso

Notice the 0 in the first column—the copy uses zero additional disk blocks. Modifications to either file write only the changed blocks.

Note: The -c flag requests cloning, but cp on macOS uses clones automatically when possible. The flag ensures failure if cloning isn’t possible (e.g., cross-volume copies).

Snapshots

Snapshots capture the state of a volume at a point in time:

# List snapshots
$ tmutil listlocalsnapshots /
Snapshots for volume group containing disk /:
com.apple.TimeMachine.2024-01-15-091234.local
com.apple.TimeMachine.2024-01-14-091234.local
...

# Or using diskutil
$ diskutil apfs listSnapshots disk3s1
Snapshots for disk3s1 (1 found)
|
+-- XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
    Snapshot Name:        com.apple.os.update-XXXXXXX
    Snapshot UUID:        XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
    Snapshot Sealed:      Yes

Time Machine creates local snapshots hourly. The system creates snapshots before updates for rollback capability.

Creating and managing snapshots:

# Create a snapshot (requires admin privileges)
$ sudo tmutil localsnapshot /
Created local snapshot with date: 2024-01-15-103045

# Delete a specific snapshot
$ sudo tmutil deletelocalsnapshots 2024-01-15-103045

# Delete all local snapshots
$ sudo tmutil deletelocalsnapshots /

Native Encryption

APFS supports encryption at multiple levels:

No encryption: Volume is unencrypted

Single-key encryption: Entire volume encrypted with one key

Multi-key encryption: Metadata encrypted with one key, file contents with per-file or per-extent keys

# Check encryption status
$ diskutil apfs list | grep -A2 "FileVault:"
    FileVault:                 Yes (Unlocked)

# Encrypt a volume
$ diskutil apfs encryptVolume disk3s5 -user disk

FileVault 2 on APFS uses native APFS encryption rather than the Core Storage layer used with HFS+.

Space Efficiency Features

Sparse files: Files with holes don’t consume space for zero-filled regions:

# Create a sparse file (1GB virtual, ~0 actual)
$ dd if=/dev/zero of=sparse.img bs=1 count=0 seek=1G 2>/dev/null
$ ls -ls sparse.img
0 -rw-r--r--  1 user  staff  1073741824 Jan 15 10:45 sparse.img

Fast directory sizing: APFS tracks directory sizes efficiently:

# This is fast on APFS
$ diskutil info disk3s5 | grep "Used"
   Container Total Space:    494.4 GB (494384795648 Bytes)...
   Volume Used Space:        256.3 GB (256300000000 Bytes)...

Atomic Safe-Save

APFS supports atomic rename operations that enable safe document saving:

  1. Write new content to a temporary file
  2. Atomically rename temporary file to replace original
  3. Original is safely replaced without corruption risk

macOS applications use this pattern via NSDocument’s save methods.

APFS Limitations

No Built-in Compression

Unlike HFS+ (which supported transparent compression) or ZFS/Btrfs (which offer compression), APFS doesn’t compress data:

# Check if a file is compressed (HFS+ compression, may still exist)
$ ls -lO file.txt
-rw-r--r--  1 user  staff  compressed  1000 Jan 15 10:50 file.txt

# APFS doesn't add new compression; existing compressed files remain compressed

No Built-in Checksumming

APFS checksums metadata but not user data. Unlike ZFS, APFS cannot detect silent data corruption (bit rot) in file contents.

Case Sensitivity Trade-offs

APFS supports case-sensitive volumes, but the default (case-insensitive) maintains compatibility with existing macOS conventions and applications.

Fusion Drive Complexity

APFS with Fusion Drives (SSD + HDD combinations) works but was added later and lacks some optimizations present on pure SSD.

Working with APFS

Creating Volumes

# Add a volume to an existing container
$ sudo diskutil apfs addVolume disk3 APFS "MyVolume"
# or with case-sensitivity
$ sudo diskutil apfs addVolume disk3 "Case-sensitive APFS" "MyVolume"

# Add an encrypted volume
$ sudo diskutil apfs addVolume disk3 APFS "SecureVolume" -passphrase

Deleting Volumes

# Delete a volume (space returns to container)
$ sudo diskutil apfs deleteVolume disk3s7

Resizing Containers

# Resize a container
$ sudo diskutil apfs resizeContainer disk3 400GB

# Resize to fill available space
$ sudo diskutil apfs resizeContainer disk3 0

Converting from HFS+

# Non-destructive conversion (if supported)
$ sudo diskutil apfs convert disk2s1

# Check conversion eligibility
$ diskutil info disk2s1 | grep "APFS"

Checking Filesystem Health

# Verify an APFS volume
$ diskutil verifyVolume disk3s1
Started file system verification on disk3s1 Macintosh HD
Verifying storage system
...
Verified storage system
Storage system check exit code is 0
Finished file system verification on disk3s1 Macintosh HD

# Repair (requires booting to Recovery for system volume)
$ sudo diskutil repairVolume disk3s5

APFS vs Other Filesystems

FeatureAPFSHFS+ext4ZFSBtrfs
Copy-on-WritePartialNoNoYesYes
SnapshotsYesNoNoYesYes
ClonesYesNoNoYesYes
Space SharingYesNoNoYesYes
Data ChecksumsMetadata onlyNoMetadataYesYes
CompressionNoLimitedNoYesYes
EncryptionNativeVia Core StorageVia LUKSYesVia dm-crypt
Flash OptimizedYesNoPartialYesYes

Summary

APFS represents a significant advancement over HFS+, bringing macOS filesystems into the modern era with:

  • Efficient space sharing through containers and volumes
  • Instant file clones and volume snapshots
  • Native encryption without external layers
  • Crash protection through copy-on-write

For Unix users accustomed to ZFS or Btrfs, APFS will feel familiar in concept but different in implementation. Understanding these differences—and accepting that APFS is optimized for different priorities (consumer devices, flash storage, tight hardware integration)—helps you work effectively with macOS storage.

HFS+ Legacy and Migration

While APFS is now the default, HFS+ (Mac OS Extended) served macOS for nearly two decades and remains relevant. You’ll encounter it on older systems, external drives, Time Machine backups from before 2017, and in specific scenarios where APFS isn’t appropriate.

HFS+ History and Design

HFS+ debuted in 1998 as an evolution of the original Hierarchical File System (HFS) from 1985. Key improvements included:

  • 32-bit allocation blocks: vs HFS’s 16-bit, allowing efficient use of large volumes
  • Unicode filenames: Up to 255 UTF-16 characters
  • Journaling: Added in Mac OS X 10.2.2 (2002) for crash recovery
  • Case sensitivity option: Added for Unix compatibility

HFS+ Variants

HFS+ comes in several flavors, identified in diskutil:

VariantDescription
Mac OS ExtendedBase HFS+ without journaling
Mac OS Extended (Journaled)Standard format with journaling enabled
Mac OS Extended (Case-sensitive)Case-sensitive variant
Mac OS Extended (Case-sensitive, Journaled)Both features
# Identify HFS+ volumes
$ diskutil list
...
   2:                  Apple_HFS Backup                   1.0 TB     disk2s2
...

$ diskutil info disk2s2 | grep "Type"
   Type (Bundle):                  hfs
   File System Personality:        Mac OS Extended (Journaled)

Why HFS+ Still Exists

Compatibility Scenarios

Older Mac hardware: Macs that can’t run macOS High Sierra or later require HFS+.

Bootable external drives for old Macs: If you need a bootable drive for older hardware.

Windows cross-compatibility: Some tools read HFS+ but not APFS. (Note: Neither is truly cross-platform without third-party tools.)

Specific applications: Some virtualization or disk imaging tools may expect HFS+.

Time Machine Archives

Time Machine backups created before APFS store data on HFS+:

# Check Time Machine drive format
$ diskutil info /Volumes/TimeMachine | grep "File System"
   File System Personality:        Mac OS Extended (Journaled)

Modern Time Machine on APFS uses snapshots differently than the hardlink-based approach on HFS+.

HFS+ Features

Journaling

The journal records filesystem changes before they occur, enabling recovery after crashes:

# Check journaling status
$ diskutil info disk2s2 | grep -i journal
   File System Personality:        Mac OS Extended (Journaled)
   Journal:                        Journal size 40960 KB at offset 0x...

# Enable journaling
$ sudo diskutil enableJournal disk2s2

# Disable journaling (not recommended)
$ sudo diskutil disableJournal disk2s2

Transparent Compression

HFS+ supports transparent file compression (introduced in Snow Leopard):

# Check if a file is compressed
$ ls -lO /bin/ls
-rwxr-xr-x  1 root  wheel  compressed  38704 Jan  1  2020 /bin/ls

# The compressed flag indicates HFS+ compression
# Actual on-disk size may be smaller than reported size

# Get actual disk usage
$ stat -f "%z bytes (logical), %b blocks (physical)" /bin/ls

Note: While APFS carries forward compressed files, it doesn’t actively compress new files.

Resource Forks and Extended Attributes

HFS+ natively supports Apple’s extended file metadata:

# View resource fork (if present)
$ ls -l /path/to/file/..namedfork/rsrc

# View extended attributes
$ xattr -l file

Working with HFS+ Volumes

Creating HFS+ Volumes

# Format as HFS+ (Journaled)
$ sudo diskutil eraseDisk JHFS+ "BackupDrive" GPT disk2

# Format a single partition
$ sudo diskutil eraseVolume JHFS+ "NewVolume" disk2s2

# Create case-sensitive HFS+
$ sudo diskutil eraseDisk JHFS+X "CaseSensitive" GPT disk2

Verifying and Repairing

# Verify HFS+ volume
$ sudo diskutil verifyVolume disk2s2
Started file system verification on disk2s2 Backup
Checking Journaled HFS Plus volume
...
The volume Backup appears to be OK
Finished file system verification on disk2s2 Backup

# Repair HFS+ volume
$ sudo diskutil repairVolume disk2s2

# For more serious issues, use fsck_hfs directly
$ sudo fsck_hfs -fy /dev/disk2s2

Command-Line Formatting with newfs_hfs

For more control, use the underlying filesystem tools:

# Create HFS+ filesystem with specific options
$ sudo newfs_hfs -v "MyVolume" -J /dev/disk2s2

# Options:
#   -v: Volume name
#   -J: Enable journaling
#   -s: Case-sensitive
#   -b: Block size

Converting HFS+ to APFS

In-Place Conversion

Apple provides non-destructive HFS+ to APFS conversion:

# Check if conversion is possible
$ diskutil info disk2s2 | grep -i "APFS"

# Convert (non-destructive, but backup first!)
$ sudo diskutil apfs convert disk2s2
Converting HFS volume to APFS...
...

Conversion requirements:

  • macOS High Sierra or later
  • Journaled HFS+ (not plain HFS+)
  • No Fusion Drive issues
  • Sufficient free space for conversion process

When Conversion Isn’t Possible

If conversion fails or isn’t desired:

# Backup, erase, restore approach
# 1. Backup all data
# 2. Erase as APFS
$ sudo diskutil eraseDisk APFS "NewDrive" GPT disk2
# 3. Restore data

HFS+ Limitations

No Space Sharing

HFS+ requires fixed partition sizes:

# Partition must be resized explicitly
$ sudo diskutil resizeVolume disk2s2 500GB

No Native Snapshots

HFS+ lacks APFS’s snapshot capability. Time Machine on HFS+ uses hard links to directories, a workaround with its own complexities.

Performance on SSD

HFS+ wasn’t designed for flash storage:

  • No TRIM support optimization
  • Journaling patterns not ideal for SSD
  • File operations less efficient than APFS

File Size and Volume Limits

LimitHFS+
Maximum volume size8 EB (theoretical)
Maximum file size8 EB (theoretical)
Maximum files~4.3 billion
Maximum filename255 characters

Practical limits are lower due to macOS constraints.

Interacting with HFS+ from Unix Perspective

Mount Options

Hbash

View mount options for HFS+ volume

$ mount | grep hfs /dev/disk2s2 on /Volumes/Backup (hfs, local, nodev, nosuid, journaled)

Mount HFS+ volume read-only

$ sudo mount -t hfs -o ro /dev/disk2s2 /Volumes/Backup


### File System Attributes

```bash
# Get HFS+ specific information
$ /usr/sbin/fstyp /dev/disk2s2
hfs

# Detailed filesystem info
$ sudo diskutil info -plist disk2s2 | plutil -p -

Common Issues

Catalog File Corruption

HFS+’s B-tree catalog can become corrupted:

# Symptoms: Files disappear, disk errors
# Solution: Run Disk Utility or fsck_hfs
$ sudo fsck_hfs -fy /dev/disk2s2

Journal Overflow

On heavily used volumes, the journal can overflow:

# Symptoms: Slow performance, errors
# Solution: Recreate journal
$ sudo diskutil disableJournal disk2s2
$ sudo diskutil enableJournal disk2s2

Permissions Repair (Deprecated)

Older macOS versions had “Repair Disk Permissions.” This is no longer relevant on modern macOS where system files are protected by SIP.

When to Use HFS+ Today

Recommended for:

  • External drives shared with older Macs
  • Time Machine destinations for older Mac backups
  • Virtual machine disk images (check VM software requirements)
  • Bootable drives for pre-High Sierra Macs

Not recommended for:

  • Primary macOS boot drives (use APFS)
  • General storage on modern Macs
  • SSD drives (APFS is better optimized)

Summary

HFS+ served macOS well for nearly 20 years, but APFS represents the future. Understanding HFS+ remains valuable for:

  • Working with legacy systems and backups
  • Troubleshooting older storage media
  • Understanding the evolution of macOS storage
  • Specific compatibility requirements

For new storage, prefer APFS unless you have a specific reason to use HFS+. The performance, reliability, and features of APFS make it the clear choice for modern Mac use.

Case Sensitivity: Options and Implications

One of the most surprising aspects of macOS for Unix users is that the filesystem is case-insensitive by default. File.txt and file.txt refer to the same file. This design choice dates back to the original Macintosh and persists today, though case-sensitive options exist.

Understanding Case Insensitivity

Case-Insensitive, Case-Preserving

macOS filesystems (APFS and HFS+) are case-insensitive but case-preserving:

# Create a file
$ echo "content" > Hello.txt

# Reference it with different case - same file!
$ cat hello.txt
content

# But the original case is preserved
$ ls
Hello.txt

# You cannot create a second file that differs only in case
$ touch HELLO.txt
touch: HELLO.txt: No such file or directory
# Actually, this silently succeeds but doesn't create a new file
$ ls
Hello.txt

Unicode Normalization

macOS also normalizes Unicode characters, treating different representations of the same character as identical:

# 'é' can be encoded as:
# - U+00E9 (precomposed: single character)
# - U+0065 U+0301 (decomposed: 'e' + combining acute accent)

# macOS treats these as the same filename

This affects filenames with accented characters, especially in cross-platform contexts.

Why Case Insensitivity?

Historical Reasons

The original Macintosh (1984) targeted non-technical users. Case sensitivity was seen as confusing—why should “Document” and “document” be different?

Practical Reasons

Many Mac applications, including Finder, assume case insensitivity:

  • File extensions: .TXT should equal .txt
  • Application bundles: Safari.app and safari.app shouldn’t be different
  • User expectations: Most users don’t think about case

Compatibility

Changing to case-sensitive by default would break:

  • Existing software assuming case insensitivity
  • User workflows and muscle memory
  • Cross-platform workflows where users expect Mac behavior

Case-Sensitive Volumes

macOS does support case-sensitive filesystems:

# Check if a volume is case-sensitive
$ diskutil info / | grep "Name (Bundle)"
   Name (Bundle):                  APFS
# Standard APFS is case-insensitive

$ diskutil info / | grep "File System Personality"
   File System Personality:        APFS
# vs "Case-sensitive APFS" for case-sensitive volumes

Creating Case-Sensitive Volumes

# Create a case-sensitive APFS volume
$ sudo diskutil apfs addVolume disk3 "Case-sensitive APFS" "CaseSensitive"

# Erase a disk with case-sensitive APFS
$ sudo diskutil eraseDisk "Case-sensitive APFS" "CaseSensitiveDisk" GPT disk2

# Create a case-sensitive HFS+
$ sudo diskutil eraseDisk JHFS+X "CaseSensitiveDisk" GPT disk2
# The X in JHFS+X denotes case-sensitive

Testing Case Sensitivity

# Check if current directory is case-sensitive
$ touch test_CASE test_case 2>/dev/null
$ count=$(ls test_* 2>/dev/null | wc -l | tr -d ' ')
$ rm -f test_CASE test_case 2>/dev/null
$ if [ "$count" = "2" ]; then
    echo "Case-sensitive"
else
    echo "Case-insensitive"
fi

Or more simply:

# This tells you the filesystem type
$ diskutil info "$(df . | tail -1 | awk '{print $1}')" | grep "Personality"

Problems with Case Insensitivity

Git and Version Control

This is the most common pain point. Repositories from case-sensitive systems (Linux) can have issues:

# On a Linux server
$ ls
Makefile
makefile

# Clone to case-insensitive Mac
$ git clone git@server:repo.git
# Only one file appears! Git sees both but filesystem conflates them

# Git status shows changed files that aren't "really" changed
$ git status
On branch main
Changes not staged for commit:
    modified:   Makefile

Workaround: Use a case-sensitive disk image for development:

# Create sparse disk image with case-sensitive filesystem
$ hdiutil create -size 50g -fs "Case-sensitive APFS" -type SPARSE -volname "DevWork" ~/dev.sparseimage

# Mount it
$ hdiutil attach ~/dev.sparseimage
/dev/disk4s1    Apple_APFS
/dev/disk5s1    APFS Volume    /Volumes/DevWork

# Clone repositories there
$ cd /Volumes/DevWork
$ git clone git@server:problematic-repo.git

Build Systems and Dependencies

Some software requires case sensitivity:

# Building software that expects case-sensitive filesystem
$ ./configure
checking for case-sensitive filesystem... no
configure: error: This package requires a case-sensitive filesystem.

Filename Conflicts

Files from case-sensitive systems may conflict:

# Archive from Linux might contain:
#   config.h
#   Config.h
#
# Extracting on macOS loses one file

# Check for conflicts before extracting
$ tar -tf archive.tar.gz | sort -f | uniq -di
# Lists filenames that would conflict

Problems with Case Sensitivity

Using a case-sensitive boot volume causes its own problems:

Adobe Software

Adobe applications (Photoshop, Illustrator, etc.) famously refuse to install on case-sensitive volumes:

"This product cannot be installed on a case-sensitive file system."

Steam and Games

Many games and game launchers assume case insensitivity:

# Game looks for "/path/to/data.dat"
# Actually stored as "/path/to/Data.dat"
# Works on Windows, fails on case-sensitive Mac

Apple’s Own Applications

Some older Apple software had issues with case-sensitive volumes. While modern macOS mostly works, testing revealed enough edge cases that Apple kept case-insensitive as default.

Best Practices

For Most Users

Keep the default case-insensitive filesystem. It’s what macOS is designed for and tested with.

For Developers

Options for handling case-sensitive requirements:

Option 1: Case-sensitive disk image (Recommended)

# Create sparse image that grows as needed
$ hdiutil create -size 100g -fs "Case-sensitive APFS" \
    -type SPARSE -volname "Code" ~/code.sparseimage

# Add to login items or create a mount script
$ cat > ~/mount-code.sh << 'EOF'
#!/bin/bash
if [ ! -d "/Volumes/Code" ]; then
    hdiutil attach ~/code.sparseimage
fi
EOF

Option 2: Separate case-sensitive volume

# Add volume to existing APFS container
$ sudo diskutil apfs addVolume disk3 "Case-sensitive APFS" "Code"

Option 3: Linux virtual machine or container

For maximum compatibility with Linux-centric projects:

# Use Docker for builds
$ docker run -v "$(pwd):/code" -w /code alpine make

For Cross-Platform Projects

When maintaining projects used on both case-sensitive and case-insensitive systems:

  1. Establish naming conventions: Always use lowercase, or establish explicit conventions
  2. Add CI checks: Detect case-only differences in commits
# Git hook to check for case conflicts
#!/bin/bash
git ls-files | sort -f | uniq -di && exit 1
  1. Document requirements: If case sensitivity matters, document it clearly

Detecting and Handling Case Issues

Find Case Conflicts in a Directory

# Find files/directories that differ only in case
$ find . -maxdepth 1 -print | sort -f | uniq -di

# More thorough check
$ find . -print | sed 's|.*/||' | sort -f | uniq -di

Git Configuration for Case Issues

# Tell Git to notice case-only renames
$ git config core.ignorecase false

# This can cause issues on case-insensitive filesystems
# as Git may report phantom changes

Safe Renaming

On case-insensitive systems, renaming with case changes requires two steps:

# This fails (same file)
$ mv File.txt file.txt

# Two-step rename
$ mv File.txt File.txt.tmp
$ mv File.txt.tmp file.txt

# Or use git for tracked files
$ git mv File.txt file.txt
# Git handles the two-step internally

Summary

Case sensitivity on macOS is a trade-off:

Case-insensitive (default) provides:

  • Maximum compatibility with Mac software
  • Familiar behavior for most users
  • Fewer surprises with existing workflows

Case-sensitive provides:

  • Linux/Unix compatibility
  • Correct handling of repositories from case-sensitive systems
  • Required by some build systems

For most users, the default case-insensitive filesystem is correct. Developers working with cross-platform code should consider case-sensitive disk images or volumes for their development work, while keeping the boot volume case-insensitive for maximum application compatibility.

Extended Attributes and Resource Forks

macOS files carry metadata beyond the basic Unix permissions and timestamps. Extended attributes (xattrs) store arbitrary key-value pairs, while resource forks—a legacy from classic Mac OS—persist in modern macOS as a special extended attribute. Understanding this metadata system is essential for proper file handling.

Extended Attributes Overview

Extended attributes are named metadata associated with files:

# List extended attributes on a file
$ xattr file.txt
com.apple.quarantine

# List with values
$ xattr -l file.txt
com.apple.quarantine: 0083;65a12345;Safari;12345678-1234-1234-1234-123456789012

# View in hex
$ xattr -px com.apple.quarantine file.txt
00 38 33 3B 36 35 61 31 32 33 34 35 3B 53 61 66
...

Common Extended Attributes

AttributePurpose
com.apple.quarantineMarks files downloaded from internet
com.apple.FinderInfoFinder metadata (color labels, etc.)
com.apple.ResourceForkLegacy resource fork data
com.apple.metadata:*Spotlight metadata
com.apple.lastuseddate#PSLast used timestamp
com.apple.maclmacOS Access Control List
com.apple.provenanceFile provenance information

The Quarantine Attribute

Files downloaded from the internet are “quarantined”:

# Download a file
$ curl -O https://example.com/file.zip

# Check quarantine status
$ xattr -l file.zip
com.apple.quarantine: 0083;65a12345;curl;12345678-...

# First time opening triggers Gatekeeper
$ open file.zip
# "file.zip" is from an unidentified developer...

Quarantine Fields

The quarantine attribute contains:

  • Flags (hex): What checks have been performed
  • Timestamp: When downloaded (hex Unix timestamp)
  • Agent: Application that downloaded it
  • UUID: Unique identifier

Removing Quarantine

# Remove quarantine from a single file
$ xattr -d com.apple.quarantine file.zip

# Remove from application bundle and contents
$ xattr -dr com.apple.quarantine /Applications/SomeApp.app

# Recursive removal for a directory
$ xattr -dr com.apple.quarantine ~/Downloads/extracted-folder/

Quarantine Flags

# Flag values:
# 0000: No quarantine (translocated app)
# 0001: Origin URL checked
# 0002: User consent obtained
# 0040: Notarization checked (macOS 10.15+)
# 0080: Hardened runtime checked

FinderInfo Attribute

Finder metadata is stored in com.apple.FinderInfo:

# View FinderInfo
$ xattr -px com.apple.FinderInfo file.txt

# This 32-byte structure contains:
# - File type and creator codes (legacy)
# - Finder flags (label color, extension hidden, etc.)
# - Position (for icon placement)

Setting Finder Tags/Labels

# Set color label using tag command (macOS 10.9+)
$ tag -a Red file.txt

# View tags
$ tag -l file.txt
file.txt    Red

# Using mdls to see color
$ mdls -name kMDItemUserTags file.txt
kMDItemUserTags = (
    "Red"
)

Note: The tag command is third-party. Install via brew install tag.

Resource Forks

Resource forks are a legacy concept from classic Mac OS where files had two “forks”:

  • Data fork: The main content (what Unix sees as the file)
  • Resource fork: Structured data (icons, code, dialog layouts, etc.)

Modern Resource Fork Access

Resource forks are stored in the com.apple.ResourceFork extended attribute:

# Check if a file has a resource fork
$ xattr file | grep ResourceFork
com.apple.ResourceFork

# Access resource fork via named fork syntax
$ ls -l file/..namedfork/rsrc

# Or via attribute
$ xattr -p com.apple.ResourceFork file | xxd | head

When Resource Forks Appear

Modern software rarely uses resource forks, but you’ll encounter them in:

  • Classic Mac OS applications/files
  • Some disk images
  • Files created by older software
  • Cross-platform transfers from old Mac files
# Get resource fork size (if any)
$ ls -l@ file | grep -A1 "ResourceFork"
    com.apple.ResourceFork       1234

Preserving Resource Forks

Standard Unix tools may not preserve resource forks:

# cp preserves extended attributes including resource forks
$ cp -p file /destination/

# tar can preserve extended attributes
$ tar --xattrs -cvf archive.tar file

# ditto preserves everything (recommended for Mac-to-Mac)
$ ditto source destination

# rsync requires special flags
$ rsync -avX source/ destination/
# -X preserves extended attributes on macOS

Working with Extended Attributes

Reading Attributes

# List attribute names
$ xattr file.txt
com.apple.quarantine
com.apple.metadata:kMDItemWhereFroms

# Print attribute value (human readable)
$ xattr -p com.apple.quarantine file.txt
0083;65a12345;Safari;...

# Print as hex
$ xattr -px com.apple.quarantine file.txt
00 38 33 3B 36 35 61 31 ...

Writing Attributes

# Set an attribute (string value)
$ xattr -w user.comment "My comment" file.txt

# Set an attribute (hex value)
$ xattr -wx user.binary "48454C4C4F" file.txt

# Verify
$ xattr -p user.comment file.txt
My comment

Removing Attributes

# Remove specific attribute
$ xattr -d com.apple.quarantine file.txt

# Remove all attributes
$ xattr -c file.txt

# Remove recursively
$ xattr -rc directory/

Copying Attributes

# Copy attributes from one file to another
$ xattr -l source.txt > /tmp/attrs
# (Manual process - no direct copy command)

# Or use ditto for full preservation
$ ditto source.txt dest.txt

Extended Attributes and Unix Tools

Tools That Preserve Attributes

ToolPreserves xattrsNotes
cpYes (with -p)macOS cp includes -p in default behavior
mvYesWithin same filesystem
rsync -XYesRequires -X flag
tar --xattrsYesRequires flag
dittoYesRecommended for Mac-to-Mac
asrYesApple Software Restore

Tools That Don’t Preserve Attributes

ToolIssueWorkaround
scpDrops attributesUse rsync -avzX
gitNot version controlledDocument requirements
curl/wgetAdds quarantineRemove with xattr -d

Attributes and SSH/SCP

When copying files to remote systems:

# Standard scp loses attributes
$ scp file.txt remote:~/

# Use rsync instead
$ rsync -avzX file.txt remote:~/

# Note: Remote must support xattrs
# Linux ext4 supports them; many remote systems do not

Metadata Spotlight Integration

Some extended attributes feed into Spotlight:

# View Spotlight metadata
$ mdls file.txt
kMDItemContentType         = "public.plain-text"
kMDItemContentTypeTree     = (
    "public.plain-text",
    "public.text",
    ...
)
kMDItemDisplayName         = "file.txt"
kMDItemFSCreatorCode       = ""
kMDItemFSFinderFlags       = 0
kMDItemFSHasCustomIcon     = (null)
...

# View WhereFroms (download URLs)
$ mdls -name kMDItemWhereFroms file.txt
kMDItemWhereFroms = (
    "https://example.com/file.txt",
    "https://example.com/"
)

Clearing Download History

# Remove WhereFroms metadata
$ xattr -d com.apple.metadata:kMDItemWhereFroms file.txt

AppleDouble Files

When copying files with extended attributes to filesystems that don’t support them (FAT32, SMB shares, etc.), macOS creates “AppleDouble” files:

# Copy file to FAT32 USB drive
$ cp file.txt /Volumes/USB_FAT32/

# Results in:
# /Volumes/USB_FAT32/file.txt        (data fork)
# /Volumes/USB_FAT32/._file.txt      (extended attributes)

The ._ files contain serialized extended attributes.

Preventing AppleDouble Files

# Use dot_clean to merge AppleDouble files
$ dot_clean /Volumes/USB_FAT32/

# Prevent creation (strip attributes during copy)
$ cp file.txt /Volumes/USB_FAT32/
$ xattr -c /Volumes/USB_FAT32/file.txt

Cleaning Up AppleDouble Files

# Find AppleDouble files
$ find /Volumes/USB_FAT32 -name '._*' -type f

# Remove them
$ find /Volumes/USB_FAT32 -name '._*' -type f -delete

# Or use dot_clean to merge them back
$ dot_clean /Volumes/USB_FAT32

Summary

Extended attributes on macOS provide:

  • Quarantine tracking for security (Gatekeeper)
  • Finder metadata for GUI integration
  • Resource forks for legacy compatibility
  • Spotlight integration for search
  • Custom metadata for applications

When working with files:

  1. Be aware that attributes exist and affect behavior
  2. Use appropriate tools (ditto, rsync -X) when preservation matters
  3. Know how to inspect (xattr -l) and remove (xattr -d) attributes
  4. Understand AppleDouble files when working with non-Mac filesystems
  5. Remember that quarantine affects executable files from the internet

Extended attributes are integral to the macOS experience, even when working from the command line.

The Metadata Files: .DS_Store and ._AppleDouble

Every Mac user eventually notices them: .DS_Store files appearing in directories, ._ prefix files cluttering USB drives, and mysterious metadata files showing up in archives. These files serve legitimate purposes but can be annoying when they leak into version control or cross-platform file sharing.

.DS_Store Files

What They Are

.DS_Store (Desktop Services Store) files are created by Finder to store custom folder attributes:

  • Window position and size
  • Icon positions (when using icon view)
  • View settings (icon/list/column/gallery view)
  • Background color or image
  • Sort order and column widths
  • Custom icons
# Find .DS_Store files
$ find ~/Documents -name '.DS_Store' -type f 2>/dev/null | head
/Users/david/Documents/.DS_Store
/Users/david/Documents/Projects/.DS_Store
...

# View basic info
$ file .DS_Store
.DS_Store: Apple Desktop Services Store

# Size is usually small
$ ls -la .DS_Store
-rw-r--r--@ 1 david  staff  6148 Jan 15 10:30 .DS_Store

.DS_Store Contents

The file format is proprietary binary, but tools exist to examine it:

# Install ds_store Python library
$ pip install ds_store

# Or use a basic hex dump
$ xxd .DS_Store | head
00000000: 0000 0001 4275 6431 0000 3000 0000 0800  ....Bud1..0.....
00000010: 0000 2000 0000 0008 0000 0000 0000 0000  .. .............
...

The file contains:

  • File/folder names in the directory
  • Per-item display settings
  • Finder view preferences
  • Spotlight comments (sometimes)

Preventing .DS_Store Creation

On Network Volumes

Finder can be configured to not create .DS_Store on network drives:

# Prevent on network volumes
$ defaults write com.apple.desktopservices DSDontWriteNetworkStores true

# Prevent on USB volumes
$ defaults write com.apple.desktopservices DSDontWriteUSBStores true

# Restart Finder to apply
$ killall Finder

This doesn’t prevent local .DS_Store creation—only network and USB volumes.

There’s no supported way to prevent local .DS_Store creation without breaking Finder functionality.

Cleaning Up .DS_Store

# Delete .DS_Store files in current directory tree
$ find . -name '.DS_Store' -type f -delete

# Dry run first (just show what would be deleted)
$ find . -name '.DS_Store' -type f -print

# Delete from a specific path, including hidden directories
$ find /path/to/folder -name '.DS_Store' -delete

.DS_Store and Git

These files should almost always be gitignored:

# Add to global gitignore
$ echo '.DS_Store' >> ~/.gitignore_global
$ git config --global core.excludesfile ~/.gitignore_global

# Add to project .gitignore
$ echo '.DS_Store' >> .gitignore

# Remove already-tracked .DS_Store files
$ git rm --cached $(git ls-files -i --exclude-standard '*.DS_Store')
$ git commit -m "Remove .DS_Store files"

AppleDouble Files (._prefix files)

What They Are

When macOS copies files to filesystems that don’t support extended attributes (FAT32, exFAT, SMB shares, etc.), it creates companion files with ._ prefix:

# Copy to FAT32 drive
$ cp photo.jpg /Volumes/USB_DRIVE/

# Results in:
$ ls -la /Volumes/USB_DRIVE/
-rwxr-xr-x  1 david  staff  1048576 Jan 15 10:30 photo.jpg
-rwxr-xr-x  1 david  staff     4096 Jan 15 10:30 ._photo.jpg

What They Contain

AppleDouble files store:

  • Extended attributes that couldn’t be preserved
  • Resource forks (if present)
  • Finder metadata
  • ACLs
# View what's in an AppleDouble file
$ xattr -l ._photo.jpg
# (Shows nothing - attributes are encoded in file content)

# Decode structure (basic view)
$ xxd ._photo.jpg | head
00000005 26 00 02 00 00 00 00 00  00 00 00 00 00 00 00 00  |&...............|
00000015 00 00 00 00 00 00 00 00  00 02 00 00 00 09 00 00  |................|
...

When They Appear

  • Copying to FAT32/exFAT USB drives
  • Copying to SMB/CIFS network shares
  • Creating ZIP files (sometimes)
  • Copying to NFS shares without xattr support
  • Burning to data CDs/DVDs

The dot_clean Command

macOS provides dot_clean to merge AppleDouble files:

# Merge AppleDouble files back into their parent files
# (Only works when copying back to APFS/HFS+)
$ dot_clean /Volumes/USB_DRIVE/

# Merge and remove AppleDouble files
$ dot_clean -m /Volumes/USB_DRIVE/

# Verbose output
$ dot_clean -v /Volumes/USB_DRIVE/
Merging ._photo.jpg
...

Cleaning Up AppleDouble Files

# Find all AppleDouble files
$ find /Volumes/USB_DRIVE -name '._*' -type f

# Delete them
$ find /Volumes/USB_DRIVE -name '._*' -type f -delete

# Delete .DS_Store AND AppleDouble files
$ find /Volumes/USB_DRIVE \( -name '.DS_Store' -o -name '._*' \) -delete

Preventing AppleDouble Files

Use ditto with –norsrc

# Copy without resource forks/AppleDouble
$ ditto --norsrc source.txt /Volumes/USB_DRIVE/

# Copy entire directory
$ ditto --norsrc ~/Documents/folder /Volumes/USB_DRIVE/folder

Strip Attributes Before Copy

# Copy then strip
$ cp file.txt /Volumes/USB_DRIVE/
$ xattr -c /Volumes/USB_DRIVE/file.txt
# This removes the need for AppleDouble companion

Use Archive Formats

When sharing files, use formats that either:

  • Don’t preserve Mac metadata (simple tar, zip)
  • Explicitly preserve it (tar with –xattrs, ditto archives)
# Create cross-platform-friendly tar
$ tar -cvf archive.tar files/
# No metadata preserved, no ._files

# Or strip metadata first
$ find files/ -name '._*' -delete
$ find files/ -name '.DS_Store' -delete
$ tar -cvf archive.tar files/

__MACOSX Folders in ZIP Files

When Finder creates ZIP files, it may include a __MACOSX folder:

# Create ZIP with Finder
$ cd ~/Documents
# Right-click folder → Compress "folder"

# Extract on Windows/Linux reveals:
# folder/
# __MACOSX/
#   folder/
#     ._file1.txt
#     ._file2.txt

Creating Clean ZIPs

# Use zip command with exclusions
$ zip -r archive.zip folder/ -x "*.DS_Store" -x "__MACOSX/*" -x "._*"

# Or use ditto to create clean ZIP
$ ditto -c -k --sequesterRsrc --keepParent folder archive.zip
# --sequesterRsrc puts resource forks in __MACOSX (standard Mac behavior)

# For truly clean ZIP (no Mac metadata at all):
$ cd folder && zip -r ../archive.zip . -x "*.DS_Store" -x "*.git*" -x "._*"

Extracting and Cleaning

# Extract ZIP
$ unzip archive.zip

# Remove Mac metadata
$ find . -name '__MACOSX' -type d -exec rm -rf {} +
$ find . -name '._*' -type f -delete
$ find . -name '.DS_Store' -type f -delete

Best Practices

For Version Control

Standard .gitignore entries:

# macOS metadata
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

For File Sharing

When sharing files with non-Mac users:

  1. Clean before sharing:
find /path/to/share -name '.DS_Store' -delete
find /path/to/share -name '._*' -delete
  1. Use appropriate tools:
# For cross-platform ZIP
zip -r archive.zip folder/ -x "*.DS_Store" -x "._*"
  1. Configure Finder:
defaults write com.apple.desktopservices DSDontWriteNetworkStores true
defaults write com.apple.desktopservices DSDontWriteUSBStores true

For External Drives

If you regularly use drives with non-Mac systems:

# Configure once
defaults write com.apple.desktopservices DSDontWriteUSBStores true

# After use, clean up
dot_clean /Volumes/DRIVE_NAME
find /Volumes/DRIVE_NAME -name '.DS_Store' -delete
find /Volumes/DRIVE_NAME -name '._*' -delete

Summary

macOS metadata files serve real purposes but require management:

File TypePurposeWhen to Remove
.DS_StoreFinder view settingsVersion control, cross-platform sharing
._* filesExtended attributes on non-Mac filesystemsWhen no longer needed, cross-platform sharing
__MACOSX/Resource forks in ZIP filesCross-platform distribution

Key commands:

  • find . -name '.DS_Store' -delete — Clean .DS_Store files
  • find . -name '._*' -delete — Clean AppleDouble files
  • dot_clean /path — Merge AppleDouble files back
  • defaults write com.apple.desktopservices DSDontWriteUSBStores true — Prevent on USB

These files are a side effect of macOS’s rich metadata system—useful locally but often unwanted when files leave the Mac ecosystem.

Filesystem Hierarchy: Where macOS Diverges

If you’re coming from Linux, you expect files to be in certain places. /etc has configuration, /var has variable data, /opt has optional packages. macOS follows some of these conventions but diverges significantly in others. Understanding the macOS filesystem hierarchy helps you find files and understand where to put things.

The macOS Root Filesystem

$ ls -la /
total 17
drwxr-xr-x   20 root  wheel    640 Dec 11 11:23 .
drwxr-xr-x   20 root  wheel    640 Dec 11 11:23 ..
lrwxr-xr-x    1 root  wheel     36 Dec 11 11:23 System -> /System/Volumes/Data/System
drwxrwxr-x   45 root  admin   1440 Jan 15 14:20 Applications
drwxr-xr-x   70 root  wheel   2240 Dec 11 11:23 Library
drwxr-xr-x@   9 root  wheel    288 Dec 11 11:23 System
drwxr-xr-x    6 root  wheel    192 Dec 11 11:23 Users
drwxr-xr-x    4 root  wheel    128 Jan 17 09:00 Volumes
drwxr-xr-x@   2 root  wheel     64 Dec 11 11:23 bin
drwxr-xr-x    2 root  wheel     64 Dec 11 11:23 cores
dr-xr-xr-x    4 root  wheel   5207 Jan 15 10:30 dev
lrwxr-xr-x@   1 root  wheel     11 Dec 11 11:23 etc -> private/etc
lrwxr-xr-x    1 root  wheel     25 Dec 11 11:23 home -> /System/Volumes/Data/home
drwxr-xr-x    2 root  wheel     64 Dec 11 11:23 opt
drwxr-xr-x    6 root  wheel    192 Dec 11 11:23 private
drwxr-xr-x@  64 root  wheel   2048 Dec 11 11:23 sbin
lrwxr-xr-x@   1 root  wheel     11 Dec 11 11:23 tmp -> private/tmp
drwxr-xr-x@  11 root  wheel    352 Dec 11 11:23 usr
lrwxr-xr-x@   1 root  wheel     11 Dec 11 11:23 var -> private/var

Note the symbolic links: /etc, /tmp, and /var are links to /private/*.

Unix-Standard Directories

/bin and /sbin

Essential system binaries:

$ ls /bin
[        cp       df       ed       ksh      ls       mv       rm       stty     zsh
bash     csh      echo     expr     launchctl mkdir    pax      rmdir    sync
cat      dash     hostname kill     link     ln       pwd      sh       tcsh     unlink
chmod    date     sleep    test     wait4path

These are Apple-provided, BSD-derived utilities. On modern macOS (Catalina+), they’re read-only, protected by System Integrity Protection, and on a sealed system volume.

/usr

User utilities and libraries:

/usr/
├── bin/          # User commands (not essential for single-user mode)
├── include/      # Header files (symlink to SDK when Xcode installed)
├── lib/          # Libraries
├── libexec/      # Support binaries for system programs
├── local/        # Local additions (Homebrew on Intel Macs)
├── sbin/         # System administration commands
├── share/        # Architecture-independent data
└── standalone/   # Standalone resources

Important: /usr/local is writable and is where user-installed software traditionally goes. Homebrew uses it on Intel Macs.

/etc (→ /private/etc)

System configuration:

$ ls /etc | head
afpovertcp.cfg
apache2
asl
asl.conf
autofs.conf
auto_home
auto_master
bashrc
bashrc_Apple_Terminal
cups

Unlike Linux, many macOS services don’t use /etc for configuration—they use property lists in /Library/Preferences or /Library/LaunchDaemons.

/var (→ /private/var)

Variable data:

$ ls /var
agentx     db         empty      folders    install    log        mail       msgs       networkd   root       rpc        run        spool      tmp        vm         yp
audit      at         backups    db         empty      folders    install    jabberd    lib        log        mail       msgs       networkd   protected  root       rpc        run        screensharing  select     spool      tmp        vm         yp

Notable contents:

  • /var/log — System logs (though modern macOS uses unified logging)
  • /var/db — System databases
  • /var/folders — Per-user temporary caches
  • /var/tmp — Temporary files that survive reboots

/tmp (→ /private/tmp)

Temporary files:

$ ls -la /tmp
lrwxr-xr-x@ 1 root  wheel  11 Dec 11 11:23 /tmp -> private/tmp

Unlike some Unix systems, /tmp may not be cleaned on every reboot. Use the proper temp directory API:

# Get user-specific temp directory
$ echo $TMPDIR
/var/folders/xx/xxxxxxxxxx/T/

# This is per-user and properly permissioned

macOS-Specific Directories

/Applications

GUI applications:

$ ls /Applications
App Store.app
Automator.app
Calculator.app
...

Applications are bundles (directories that appear as files in Finder):

$ file /Applications/Safari.app
/Applications/Safari.app: directory

$ ls /Applications/Safari.app/Contents/
_CodeSignature  Frameworks      Info.plist      MacOS           PkgInfo         Resources       XPCServices

/Library

System-wide resources and configuration:

/Library/
├── Application Support/   # App-specific data (system-wide)
├── Audio/                 # Audio plug-ins and presets
├── Caches/               # System caches
├── ColorPickers/         # Color picker modules
├── Components/           # System components
├── Contextual Menu Items/
├── CoreMediaIO/          # Video device drivers
├── Desktop Pictures/     # System wallpapers
├── Documentation/        # System documentation
├── Extensions/           # Kernel extensions (deprecated)
├── Fonts/                # System-wide fonts
├── Frameworks/           # System-wide frameworks
├── Graphics/             # Graphics drivers
├── Image Capture/        # Scanner plug-ins
├── Input Methods/        # Input method editors
├── Internet Plug-Ins/    # Browser plug-ins
├── iTunes/               # iTunes-related resources
├── Java/                 # Java installations
├── Keyboard Layouts/     # Custom keyboard layouts
├── Keychains/            # System keychain
├── LaunchAgents/         # System-wide login agents
├── LaunchDaemons/        # System daemons
├── Logs/                 # Application logs
├── Mail/                 # Mail.app resources
├── OpenDirectory/        # Directory services
├── PDF Services/         # PDF workflow actions
├── Perl/                 # Perl modules
├── Preferences/          # System-wide preferences
├── Printers/             # Printer drivers
├── PrivilegedHelperTools/ # Helper tools running as root
├── Python/               # Python packages
├── QuickLook/            # QuickLook generators
├── QuickTime/            # QuickTime components
├── Receipts/             # Package receipts
├── Ruby/                 # Ruby gems
├── Sandbox/              # Sandboxing profiles
├── Screen Savers/        # Screen savers
├── ScriptingAdditions/   # AppleScript additions
├── Scripts/              # System scripts
├── Security/             # Security resources
├── Speech/               # Speech recognition/synthesis
├── Spelling/             # Spelling dictionaries
├── Spotlight/            # Spotlight importers
├── StartupItems/         # Legacy startup items (deprecated)
├── SystemMigration/      # Migration assistant data
├── SystemProfiler/       # System profiler plugins
├── Updates/              # Software update data
├── User Pictures/        # User account pictures
├── Video/                # Video drivers
└── WebServer/            # Apache web server files

/System/Library

Apple’s system resources—protected by SIP:

$ ls /System/Library | head
Accessibility
Accounts
Address Book Plug-Ins
Ambiances
AppleRAIDConsoleUI
Assistants
Audio
Automator
BridgeSupport
CFMSupport

Never modify files here. Changes are blocked by System Integrity Protection.

~/Library

Per-user resources (in your home directory):

~/Library/
├── Application Support/   # App-specific data
├── Caches/               # App caches (safe to delete)
├── Containers/           # Sandboxed app data
├── Cookies/              # Browser cookies
├── Fonts/                # User-installed fonts
├── Group Containers/     # Shared sandboxed data
├── Keychains/            # User keychains
├── LaunchAgents/         # User login agents
├── Logs/                 # User app logs
├── Mail/                 # Mail.app data
├── Messages/             # Messages.app data
├── Preferences/          # User preferences (plists)
├── Saved Application State/ # App state for restore
└── Services/             # User-installed services

This directory is hidden in Finder by default. Access it:

# From Terminal
$ open ~/Library

# Or in Finder: Go → Go to Folder → ~/Library
# Or hold Option while clicking Go menu

/System/Volumes

macOS Catalina+ uses multiple APFS volumes:

$ ls /System/Volumes
Data        Preboot     Recovery    Update      VM
  • Data: User data, applications, mutable system data
  • Preboot: Boot-time data
  • Recovery: Recovery OS
  • Update: Software update staging
  • VM: Swap files

The system uses “firmlinks” to make these volumes appear as a unified filesystem.

/Volumes

Mount point for all volumes:

$ ls /Volumes
Macintosh HD            # Boot volume
Macintosh HD - Data     # Data volume (if visible)
ExternalDrive           # External drives appear here
DiskImage               # Mounted disk images

Comparison with FHS (Linux)

PurposeFHS (Linux)macOS
User binaries/usr/bin/usr/bin
System binaries/sbin/sbin
Config files/etc/etc + /Library/Preferences
Variable data/var/var (→ /private/var)
Temp files/tmp/tmp (→ /private/tmp) + $TMPDIR
User homes/home/Users
Root’s home/root/var/root
Mount points/mnt, /media/Volumes
Optional packages/opt/opt (mostly unused)
Third-party/opt, /usr/local/Applications, /usr/local
Libraries/usr/lib/usr/lib + /Library/Frameworks
Headers/usr/include/usr/include (SDK symlink)
Docs/usr/share/doc/Library/Documentation

Homebrew Locations

Homebrew has different install locations:

Intel Macs

/usr/local/
├── bin/         # Symlinks to installed binaries
├── Cellar/      # Installed formula versions
├── etc/         # Configuration files
├── include/     # Header files
├── lib/         # Libraries
├── opt/         # Symlinks to latest versions
├── sbin/        # System binaries
├── share/       # Architecture-independent data
└── var/         # Variable data (logs, databases)

Apple Silicon Macs

/opt/homebrew/
├── bin/
├── Cellar/
├── etc/
├── include/
├── lib/
├── opt/
├── sbin/
├── share/
└── var/

The different location avoids conflicts with Rosetta 2 (Intel emulation).

Finding Files

Using locate (if enabled)

# Enable locate database
$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.locate.plist

# Update database
$ sudo /usr/libexec/locate.updatedb

# Find files
$ locate nginx.conf

Using mdfind (Spotlight)

# Find by name
$ mdfind -name "nginx.conf"

# Find by content
$ mdfind "error handling"

# Find by type
$ mdfind "kMDItemKind == 'Application'"

Using find

# Traditional Unix find
$ find /usr -name "*.h" -type f 2>/dev/null

Summary

The macOS filesystem hierarchy combines Unix traditions with Apple innovations:

Unix-familiar:

  • /bin, /sbin, /usr, /etc, /var, /tmp work as expected
  • /usr/local is available for local software

macOS-specific:

  • /Applications for GUI applications
  • /Library hierarchy for system/user resources
  • /System protected by SIP
  • /Volumes for all mounted volumes
  • /Users instead of /home
  • /private containing the actual /etc, /var, /tmp

Key differences from Linux:

  • Configuration often in property lists, not /etc text files
  • Three-tier Library structure (/System/Library, /Library, ~/Library)
  • Homebrew in /opt/homebrew on Apple Silicon
  • Multiple APFS volumes presenting as unified filesystem

Understanding this hierarchy helps you navigate macOS, find configuration files, and work effectively across Unix and macOS environments.

Disk Management from the Command Line

While Disk Utility provides a graphical interface for disk operations, the command line offers more power and precision. macOS provides diskutil as the primary disk management tool, along with lower-level utilities for specific tasks.

The diskutil Command

diskutil is macOS’s comprehensive disk management utility:

# Get help
$ diskutil
Disk Utility Tool
Utility to manage local disks and volumes
...

# List all verbs (commands)
$ diskutil listFilesystems
$ diskutil list
$ diskutil info
...

Listing Disks and Volumes

# List all disks and partitions
$ diskutil list
/dev/disk0 (internal):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *500.1 GB   disk0
   1:             Apple_APFS_ISC Container disk1         524.3 MB   disk0s1
   2:                 Apple_APFS Container disk3         494.4 GB   disk0s2
   3:        Apple_APFS_Recovery Container disk2         5.4 GB     disk0s3

/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +494.4 GB   disk3
   1:                APFS Volume Macintosh HD            10.1 GB    disk3s1
   2:              APFS Snapshot com.apple.os.update-... 10.1 GB    disk3s1s1
   3:                APFS Volume Preboot                 5.3 GB     disk3s2
   4:                APFS Volume Recovery                1.6 GB     disk3s3
   5:                APFS Volume Data                    258.2 GB   disk3s5
   6:                APFS Volume VM                      3.2 GB     disk3s6

# List with more detail
$ diskutil list -plist  # Machine-readable
$ diskutil list external # Only external disks
$ diskutil list internal # Only internal disks

Disk and Volume Information

# Detailed information about a disk
$ diskutil info disk0
   Device Identifier:         disk0
   Device Node:               /dev/disk0
   Whole:                     Yes
   Part of Whole:             disk0
   Device / Media Name:       APPLE SSD AP0512Q

   Volume Name:               Not applicable (no file system)
   Mounted:                   Not applicable (no file system)
   File System:               None

   Content (IOContent):       GUID_partition_scheme
   OS Can Be Installed:       No
   Media Type:                Generic
   Protocol:                  Apple Fabric
   SMART Status:              Verified
   Disk Size:                 500.1 GB (500107862016 Bytes)
   ...

# Information about a volume
$ diskutil info disk3s5
   Device Identifier:         disk3s5
   Device Node:               /dev/disk3s5
   Whole:                     No
   Part of Whole:             disk3

   Volume Name:               Data
   Mounted:                   Yes
   Mount Point:               /System/Volumes/Data

   File System Personality:   APFS
   Type (Bundle):             apfs
   Name (User Visible):       APFS
   Owners:                    Enabled
   ...

# Information about current volume
$ diskutil info /

Mounting and Unmounting

# Unmount a volume (leaves disk attached)
$ diskutil unmount disk3s5
Volume Data on disk3s5 unmounted

# Mount a volume
$ diskutil mount disk3s5
Volume Data on disk3s5 mounted

# Mount at specific location
$ diskutil mount -mountPoint /Volumes/MyMount disk3s5

# Unmount entire disk (all volumes)
$ diskutil unmountDisk disk2
Unmount of all volumes on disk2 was successful

# Force unmount (use with caution)
$ diskutil unmount force disk3s5

Ejecting Disks

# Eject (unmount and remove)
$ diskutil eject disk2
Disk disk2 ejected

# This is equivalent to ejecting in Finder

Formatting and Erasing

Erasing a Volume

# Erase and reformat a volume
$ sudo diskutil eraseVolume APFS "NewVolume" disk2s2

# Specify filesystem type:
#   APFS - Apple File System
#   JHFS+ - Mac OS Extended (Journaled)
#   ExFAT - Cross-platform
#   MS-DOS FAT32 - Legacy compatible

Erasing an Entire Disk

# Erase and partition entire disk with APFS
$ sudo diskutil eraseDisk APFS "MyDisk" GPT disk2
Started erase on disk2
Unmounting disk
Creating the partition map
Waiting for partitions to activate
Formatting disk2s2 as APFS with name MyDisk
...
Finished erase on disk2

# Options:
#   GPT - GUID Partition Table (modern, recommended)
#   MBR - Master Boot Record (legacy, limited)
#   APM - Apple Partition Map (PowerPC era)

Available Filesystems

$ diskutil listFilesystems
Formattable file systems

These file system personalities can be used for erasing and partitioning.
...
-------------------------------------------------------------------------------
PERSONALITY                      USER VISIBLE NAME
-------------------------------------------------------------------------------
ExFAT                            ExFAT
Free Space                       Free Space
(or) free
MS-DOS                           MS-DOS (FAT)
MS-DOS FAT12                     MS-DOS (FAT12)
MS-DOS FAT16                     MS-DOS (FAT16)
MS-DOS FAT32                     MS-DOS (FAT32)
(or) fat32
HFS+                             Mac OS Extended
Case-sensitive HFS+              Mac OS Extended (Case-sensitive)
Case-sensitive Journaled HFS+    Mac OS Extended (Case-sensitive, Journaled)
(or) jhfsx
Journaled HFS+                   Mac OS Extended (Journaled)
(or) jhfs+
APFS                             APFS
(or) apfs
Case-sensitive APFS              APFS (Case-sensitive)
(or) apfsx

APFS-Specific Operations

Working with Containers

# List APFS containers
$ diskutil apfs list
APFS Container (1 found)
|
+-- Container disk3 494.4 GB
    ====================================================
    APFS Container Reference:     disk3
    Size (Capacity Ceiling):      494384795648 B (494.4 GB)
    Capacity In Use By Volumes:   277712732160 B (277.7 GB) (56.2%)
    Capacity Not Allocated:       216672063488 B (216.7 GB) (43.8%)
    |
    +-< Physical Store disk0s2 494.4 GB
    |   ---------------------------------------------------
    |   APFS Physical Store Disk:   disk0s2
    |   Size:                       494384795648 B (494.4 GB)
    |
    +-> Volume disk3s1 Macintosh HD
    ...

# Create a new APFS container
$ sudo diskutil apfs createContainer disk2s2

# Resize a container
$ sudo diskutil apfs resizeContainer disk3 400GB

Working with Volumes

# Add volume to container
$ sudo diskutil apfs addVolume disk3 APFS "NewVolume"

# Add encrypted volume
$ sudo diskutil apfs addVolume disk3 APFS "SecureVolume" -passphrase

# Delete volume
$ sudo diskutil apfs deleteVolume disk3s7

# List volumes in container
$ diskutil apfs listVolumes disk3

Encryption

# Check encryption status
$ diskutil apfs list | grep -A5 "FileVault"

# Encrypt a volume
$ sudo diskutil apfs encryptVolume disk3s5 -user disk

# Decrypt a volume
$ sudo diskutil apfs decryptVolume disk3s5

# Change encryption password
$ sudo diskutil apfs changePassphrase disk3s5

Snapshots

# List snapshots
$ diskutil apfs listSnapshots disk3s1
Snapshots for disk3s1 (2 found)
|
+-- 8B6C8F4E-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|   Snapshot Name:        com.apple.os.update-XXXX
|   Snapshot UUID:        8B6C8F4E-XXXX-XXXX-XXXX-XXXXXXXXXXXX
...

# Delete a snapshot
$ sudo diskutil apfs deleteSnapshot disk3s1 -uuid 8B6C8F4E-XXXX-XXXX-XXXX-XXXXXXXXXXXX

Partitioning

Creating Partitions

# Partition a disk with multiple partitions
$ sudo diskutil partitionDisk disk2 GPT \
    APFS "Data" 100GB \
    APFS "Backup" 100GB \
    "Free Space" "Available" R

# R = Remainder (use remaining space)

Resizing Partitions

# Resize a partition (grow or shrink)
$ sudo diskutil resizeVolume disk2s2 50GB

# Resize using limits (fill available space)
$ sudo diskutil resizeVolume disk2s2 R

Adding Partitions

# Add a partition using free space after existing partition
$ sudo diskutil addPartition disk2s2 APFS "NewPartition" 50GB

Merging Partitions

# Merge partitions (data on second partition is lost)
$ sudo diskutil mergePartitions JHFS+ "MergedVolume" disk2s2 disk2s3

Disk Images

Creating Disk Images

# Create empty disk image
$ hdiutil create -size 100m -fs APFS -volname "TestImage" test.dmg

# Create from folder
$ hdiutil create -srcfolder /path/to/folder -volname "Backup" backup.dmg

# Create encrypted disk image
$ hdiutil create -size 1g -fs APFS -encryption AES-256 -volname "Secure" secure.dmg

# Create sparse image (grows as needed)
$ hdiutil create -size 10g -fs APFS -type SPARSE -volname "Sparse" sparse.sparseimage

# Create sparse bundle (grows as bundle of files)
$ hdiutil create -size 10g -fs APFS -type SPARSEBUNDLE -volname "Bundle" bundle.sparsebundle

Mounting and Unmounting Images

# Mount disk image
$ hdiutil attach image.dmg
/dev/disk4          GUID_partition_scheme
/dev/disk4s1        Apple_APFS
/dev/disk5s1        Apple_APFS                      /Volumes/ImageVolume

# Mount read-only
$ hdiutil attach -readonly image.dmg

# Mount without Finder window
$ hdiutil attach -nobrowse image.dmg

# Unmount/detach
$ hdiutil detach /dev/disk4
# Or
$ hdiutil detach /Volumes/ImageVolume

Converting Images

# Convert to read-only compressed DMG
$ hdiutil convert source.dmg -format UDZO -o compressed.dmg

# Formats:
#   UDRO - Read-only
#   UDZO - Compressed (read-only)
#   UDBZ - bzip2 compressed
#   UDTO - DVD/CD master
#   UDSB - Sparsebundle

Resizing Images

# Resize sparse image
$ hdiutil resize -size 20g sparse.sparseimage

# Resize to minimum
$ hdiutil resize -size min sparse.sparseimage

# Compact sparse image (reclaim space)
$ hdiutil compact sparse.sparseimage

Verification and Repair

Verifying Filesystems

# Verify volume
$ diskutil verifyVolume disk3s5
Started file system verification on disk3s5 Data
Verifying storage system
Checking volume
...
Verified storage system
Storage system check exit code is 0
Finished file system verification on disk3s5 Data

# Verify disk
$ diskutil verifyDisk disk3

Repairing Filesystems

# Repair volume (must be unmounted for non-live repair)
$ sudo diskutil repairVolume disk3s5

# For boot volume, use Recovery Mode
# or First Aid in Disk Utility

Using fsck Directly

# Check APFS volume
$ sudo fsck_apfs -n /dev/disk3s5
# -n = no changes (dry run)

# Repair APFS
$ sudo fsck_apfs -y /dev/disk3s5

# Check HFS+
$ sudo fsck_hfs -n /dev/disk2s2

Secure Erase

Secure Erase Options

# Note: Secure erase is less effective on SSDs due to wear leveling
# For SSDs, use FileVault encryption instead

# Zero out free space on volume
$ diskutil secureErase freespace 0 /Volumes/DiskName
# Levels: 0=zero, 1=random, 2=US DoD 7-pass, 3=Gutmann 35-pass, 4=US DoE 3-pass

# Securely erase entire disk
$ diskutil secureErase 0 disk2

For SSDs

# Enable FileVault for secure deletion via encryption
# Then simply erase - data is unrecoverable without key

# Check TRIM status
$ system_profiler SPSerialATADataType | grep TRIM

Practical Examples

Format USB Drive for Cross-Platform Use

# ExFAT: Works on macOS, Windows, Linux
$ sudo diskutil eraseDisk ExFAT "USB_DRIVE" MBR disk2

# Use MBR for maximum compatibility with older systems

Create Bootable macOS Installer

# Download macOS from App Store, then:
$ sudo /Applications/Install\ macOS\ Sonoma.app/Contents/Resources/createinstallmedia \
    --volume /Volumes/USB_DRIVE

Clone a Volume

# Using asr (Apple Software Restore)
$ sudo asr restore --source /Volumes/Source --target /Volumes/Target --erase

# Using dd (byte-for-byte copy - use with extreme caution)
$ sudo dd if=/dev/rdisk2 of=/dev/rdisk3 bs=1m

# Note: Use 'r' prefix (rdisk) for raw device, faster

Check Disk Health

# SMART status
$ diskutil info disk0 | grep SMART
   SMART Status:              Verified

# More detailed SMART info
$ brew install smartmontools
$ sudo smartctl -a /dev/disk0

Summary

macOS disk management from the command line:

TaskCommand
List disksdiskutil list
Disk infodiskutil info disk0
Mount volumediskutil mount disk3s5
Unmount volumediskutil unmount disk3s5
Eject diskdiskutil eject disk2
Format diskdiskutil eraseDisk APFS "Name" GPT disk2
Format volumediskutil eraseVolume APFS "Name" disk2s2
Add APFS volumediskutil apfs addVolume disk3 APFS "Name"
Create disk imagehdiutil create -size 1g -fs APFS name.dmg
Mount disk imagehdiutil attach image.dmg
Verify filesystemdiskutil verifyVolume disk3s5
Repair filesystemdiskutil repairVolume disk3s5

The diskutil command provides comprehensive disk management while hdiutil handles disk images. For most operations, these tools replace the need for lower-level utilities while providing macOS-appropriate behavior.

Volumes, Containers, and Snapshots

APFS introduces a new storage hierarchy that differs fundamentally from traditional partitioning. Understanding containers, volumes, and snapshots is essential for effective storage management on modern macOS.

The APFS Storage Hierarchy

Physical Disk
    │
    └── Partition (APFS Container)
            │
            ├── Volume A ─── Snapshot 1
            │                Snapshot 2
            ├── Volume B
            └── Volume C

Unlike traditional partitioning where each partition has a fixed size, APFS volumes share space within a container dynamically.

Understanding Containers

What Is a Container?

An APFS container is:

  • Equivalent to a partition in terms of disk allocation
  • A pool of storage that multiple volumes share
  • The unit of encryption (container-level encryption wraps all volumes)
# View containers
$ diskutil apfs list
APFS Container (1 found)
|
+-- Container disk3 494.4 GB
    ====================================================
    APFS Container Reference:     disk3
    Size (Capacity Ceiling):      494384795648 B (494.4 GB)
    Capacity In Use By Volumes:   277848256512 B (277.8 GB) (56.2%)
    Capacity Not Allocated:       216536539136 B (216.5 GB) (43.8%)
    |
    +-< Physical Store disk0s2 494.4 GB

Container vs Partition

Traditional PartitionAPFS Container
Fixed sizeFixed size
One filesystemMultiple volumes
Resizing requires unmountVolumes resize dynamically
Independent of other partitionsVolumes share space

Multiple Physical Stores

A container can span multiple physical devices (like Fusion Drive):

# Fusion Drive example
+-< Physical Store disk0s2 (SSD) 128.0 GB
+-< Physical Store disk1s2 (HDD) 1.0 TB

This allows APFS to move data between fast and slow storage automatically.

Working with Volumes

Volume Roles

APFS assigns roles that define volume behavior:

RolePurposeCharacteristics
SystemBoot OSRead-only, sealed
DataUser dataRead-write
VMSwap spaceNot persistent
PrebootBoot helpersSmall, specific files
RecoveryRecovery OSHidden, bootable
# View volume roles
$ diskutil apfs list | grep "Role:"
    APFS Volume Disk (Role):   disk3s1 (System)
    APFS Volume Disk (Role):   disk3s5 (Data)
    APFS Volume Disk (Role):   disk3s6 (VM)

The System-Data Volume Pair

Modern macOS uses a “firmlinked” pair:

System Volume (read-only)     Data Volume (read-write)
/System                       /Users
/Library (system)             /Library (user)
/bin, /sbin, /usr            /Applications

Firmlinks make these appear as a single filesystem:

# These paths are on different volumes but appear unified
$ ls /
Applications  Library  System  Users  ...

# Check which volume a path is on
$ df /Users
Filesystem      512-blocks      Used Available Capacity  Mounted on
/dev/disk3s5   966871088 532846072 422952016    56%    /System/Volumes/Data

$ df /System
Filesystem     512-blocks     Used Available Capacity  Mounted on
/dev/disk3s1   966871088 19782344 422952016     5%    /

Creating Volumes

# Add a standard volume
$ sudo diskutil apfs addVolume disk3 APFS "DataVolume"
Exporting new APFS Volume "DataVolume" from APFS Container Reference disk3
Started APFS operation on disk3
Preparing to add APFS Volume to APFS Container disk3
Creating APFS Volume
...
Mounting APFS Volume
APFS Volume created and mounted at /Volumes/DataVolume
Finished APFS operation on disk3

# Add case-sensitive volume
$ sudo diskutil apfs addVolume disk3 "Case-sensitive APFS" "CaseSensitive"

# Add encrypted volume
$ sudo diskutil apfs addVolume disk3 APFS "Encrypted" -passphrase
Enter new passphrase:
Re-enter new passphrase:
...

# Add volume with quota (space limit)
$ sudo diskutil apfs addVolume disk3 APFS "Limited" -quota 10g

# Add volume with reserve (guaranteed minimum space)
$ sudo diskutil apfs addVolume disk3 APFS "Reserved" -reserve 5g

Volume Quotas and Reserves

# Set quota on existing volume (maximum space)
$ sudo diskutil apfs setQuota disk3s7 10g

# Set reserve on existing volume (guaranteed space)
$ sudo diskutil apfs setReserve disk3s7 5g

# Remove quota
$ sudo diskutil apfs setQuota disk3s7 0

# View settings
$ diskutil apfs list | grep -A10 "Volume disk3s7"
    +-> Volume disk3s7 DataVolume
        ---------------------------------------------------
        APFS Volume Disk (Role):   disk3s7 (No specific role)
        Quota:                     10.0 GB (10737418240 Bytes)
        Reserve:                   5.0 GB (5368709120 Bytes)

Deleting Volumes

# Delete a volume (space returns to container pool)
$ sudo diskutil apfs deleteVolume disk3s7
Started APFS operation on disk3s7 DataVolume
Deleting APFS Volume from APFS Container disk3
...
Finished APFS operation on disk3s7 DataVolume

Warning: This permanently destroys all data on the volume.

Renaming Volumes

# Rename a volume
$ sudo diskutil rename disk3s5 "New Name"

# Or
$ sudo diskutil apfs rename disk3s5 "New Name"

Working with Snapshots

What Are Snapshots?

Snapshots capture the state of a volume at a point in time:

  • Read-only view of the filesystem
  • Space-efficient (only stores changes)
  • Created instantly
  • Used by Time Machine and system updates

Viewing Snapshots

# List snapshots with tmutil
$ tmutil listlocalsnapshots /
Snapshots for volume group containing disk /:
com.apple.TimeMachine.2024-01-15-091234.local
com.apple.TimeMachine.2024-01-15-101234.local
com.apple.TimeMachine.2024-01-15-111234.local

# List with diskutil
$ diskutil apfs listSnapshots disk3s1
Snapshots for disk3s1 (2 found)
|
+-- 5A7C2E48-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|   Snapshot Name:        com.apple.os.update-XXXX
|   Snapshot UUID:        5A7C2E48-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|   Snapshot Sealed:      Yes
|
+-- 8B6C8F4E-XXXX-XXXX-XXXX-XXXXXXXXXXXX
    Snapshot Name:        com.apple.TimeMachine.2024-01-15-091234.local
    Snapshot UUID:        8B6C8F4E-XXXX-XXXX-XXXX-XXXXXXXXXXXX

Time Machine Local Snapshots

Time Machine creates hourly local snapshots:

# Create a local snapshot manually
$ sudo tmutil localsnapshot
Created local snapshot with date: 2024-01-15-143045

# View space used by local snapshots
$ tmutil listlocalsnapshots / | wc -l
      24

# Delete specific snapshot
$ sudo tmutil deletelocalsnapshots 2024-01-15-143045
Deleted local snapshot '2024-01-15-143045'

# Delete all local snapshots
$ sudo tmutil deletelocalsnapshots /
Deleted 24 Time Machine local snapshots

System Update Snapshots

macOS creates snapshots before system updates:

$ diskutil apfs listSnapshots disk3s1 | grep "update"
    Snapshot Name:        com.apple.os.update-XXXX

These allow rollback if an update causes problems.

Mounting Snapshots

# Mount a snapshot read-only
$ sudo mkdir /tmp/snapshot_mount
$ sudo mount_apfs -s com.apple.TimeMachine.2024-01-15-091234.local /dev/disk3s5 /tmp/snapshot_mount

# Browse the snapshot
$ ls /tmp/snapshot_mount

# Unmount
$ sudo umount /tmp/snapshot_mount

Snapshot Space Management

Snapshots don’t immediately use space, but they prevent space reclamation:

# View purgeable space (includes snapshot data)
$ diskutil apfs list | grep -A5 "Container disk3"
    Size (Capacity Ceiling):      494384795648 B (494.4 GB)
    Capacity In Use By Volumes:   277848256512 B (277.8 GB) (56.2%)
    Capacity Not Allocated:       216536539136 B (216.5 GB) (43.8%)

# When deleting files, space may not free until snapshots are removed

Deleting Snapshots

# Delete by name
$ sudo diskutil apfs deleteSnapshot disk3s5 -name "com.apple.TimeMachine.2024-01-15-091234.local"

# Delete by UUID
$ sudo diskutil apfs deleteSnapshot disk3s5 -uuid 8B6C8F4E-XXXX-XXXX-XXXX-XXXXXXXXXXXX

# Delete all Time Machine local snapshots (frees space)
$ sudo tmutil deletelocalsnapshots /

Sealed System Volumes

macOS Big Sur+ uses a “sealed” system volume:

$ diskutil apfs list | grep -A3 "Macintosh HD"
    +-> Volume disk3s1 Macintosh HD
        ---------------------------------------------------
        APFS Volume Disk (Role):   disk3s1 (System)
        Snapshot Sealed:           Yes

What Sealing Means

  • Cryptographic verification of system files
  • Any modification breaks the seal
  • Ensures system integrity
  • Snapshot contains known-good state

Sealed Snapshot Boot

The system actually boots from a sealed snapshot:

# Current boot is a snapshot
$ mount | grep "disk3s1"
/dev/disk3s1s1 on / (apfs, sealed, local, read-only, journaled)
#            ^^ Note the 's1' suffix indicating snapshot

Container Operations

Resizing Containers

# Shrink container to specific size
$ sudo diskutil apfs resizeContainer disk3 400GB

# Expand container to fill available space
$ sudo diskutil apfs resizeContainer disk3 0

Creating New Containers

# On a new disk or partition
$ sudo diskutil apfs createContainer disk2s2
Started APFS operation on disk2s2
Creating new APFS Container
...

Deleting Containers

# Delete container (destroys all volumes!)
$ sudo diskutil apfs deleteContainer disk3

Warning: This destroys all data in the container.

Practical Scenarios

Create a Development Volume

# Case-sensitive volume for cross-platform development
$ sudo diskutil apfs addVolume disk3 "Case-sensitive APFS" "Development" -quota 100g

# Will mount at /Volumes/Development
$ cd /Volumes/Development
$ git clone git@github.com:user/project.git

Create an Encrypted Work Volume

# Encrypted volume for sensitive data
$ sudo diskutil apfs addVolume disk3 APFS "WorkSecure" -passphrase

# Lock/unlock manually
$ diskutil apfs lockVolume disk3s7
$ diskutil apfs unlockVolume disk3s7

Recover Space from Snapshots

# Check space
$ df -h /
Filesystem       Size   Used  Avail Capacity  Mounted on
/dev/disk3s1s1  460Gi  256Gi  180Gi    59%    /

# Delete unnecessary snapshots
$ sudo tmutil deletelocalsnapshots /
$ sudo tmutil deletelocalsnapshots /System/Volumes/Data

# Verify space recovered
$ df -h /

Browse Past File Versions

# Find available snapshots
$ tmutil listlocalsnapshots /System/Volumes/Data

# Mount a snapshot
$ sudo mkdir /tmp/old_data
$ sudo mount_apfs -s com.apple.TimeMachine.2024-01-14-091234.local /dev/disk3s5 /tmp/old_data

# Browse old versions
$ ls /tmp/old_data/Users/david/Documents/

# Copy needed files
$ cp /tmp/old_data/Users/david/Documents/important.doc ~/Desktop/

# Unmount
$ sudo umount /tmp/old_data

Summary

APFS storage hierarchy:

ConceptDescriptionKey Commands
ContainerPool of storagediskutil apfs list, createContainer, deleteContainer
VolumeFilesystem within containeraddVolume, deleteVolume, setQuota
SnapshotPoint-in-time capturelistSnapshots, deleteSnapshot, tmutil
RoleVolume purpose (System, Data, VM)View with diskutil apfs list

Key advantages of APFS:

  • Space sharing: No need to pre-size volumes
  • Instant snapshots: Capture state without copying
  • Fast clones: Duplicate files without using space
  • Encryption: Built into the filesystem

Understanding this hierarchy helps you manage storage effectively, recover from issues, and take advantage of features that traditional Unix filesystems don’t offer.

Mastering the macOS Shell

The shell is your primary interface to macOS’s Unix power. While the graphical interface handles most daily tasks, the shell provides capabilities that no GUI can match: automation, text processing, remote access, and precise control over system operations.

macOS’s shell environment has evolved significantly, most notably with the transition from bash to zsh as the default shell in Catalina (2019). Understanding this environment—and its macOS-specific characteristics—is essential for effective command-line work.

Why the Shell Matters

For Unix users, the shell is familiar territory. But macOS’s shell environment has unique characteristics:

  • Default shell change: zsh replaced bash as the default in 2019
  • BSD commands: The underlying utilities differ from GNU/Linux
  • macOS integration: Clipboard, Notification Center, and GUI apps accessible from shell
  • Path complexity: Framework libraries, Homebrew, and Xcode tools all need PATH attention
  • Security restrictions: SIP and privacy controls affect shell capabilities

Shell Basics for Mac Newcomers

If you’re new to Unix shells, here’s the essential context:

A shell is a program that interprets your commands and executes them. It provides:

  • Command execution
  • Script interpretation
  • Environment variable management
  • Input/output redirection
  • Job control

macOS includes several shells:

$ cat /etc/shells
# List of acceptable shells
/bin/bash
/bin/csh
/bin/dash
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh

What You’ll Learn in This Part

The Great Shell Transition: Bash to Zsh explains why Apple changed the default shell, what the differences are, and how to adapt your workflows.

Terminal.app Deep Dive covers Apple’s built-in terminal emulator, including its features, configuration, and capabilities that many users never discover.

iTerm2 and Alternative Terminals explores third-party terminal emulators that offer features beyond Terminal.app.

Shell Configuration on macOS teaches you to configure zsh and bash effectively, understanding which files are loaded when.

Environment Variables and PATH addresses one of the most common sources of confusion: getting your PATH right across different contexts.

macOS-Specific Shell Features introduces shell capabilities unique to macOS, from clipboard integration to speaking text.

Shell Integration with macOS Services shows how to connect command-line work with Finder, Spotlight, Keychain, and other system services.

Terminal Quick Start

If you’re new to the Mac terminal:

  1. Open Terminal: Press Cmd+Space, type “Terminal”, press Enter
  2. You’re now in zsh: The default shell since Catalina
  3. Your home directory: Terminal starts in ~ (your home folder)
  4. Basic commands work: ls, cd, pwd, cat, grep, etc.
# Where am I?
$ pwd
/Users/yourname

# What's here?
$ ls -la

# Go somewhere
$ cd Documents

# Run something
$ echo "Hello, macOS Unix!"
Hello, macOS Unix!

From here, the following chapters will help you master the macOS shell environment.

The Great Shell Transition: Bash to Zsh

In macOS Catalina (2019), Apple changed the default shell from bash to zsh. This was one of the most significant changes for command-line users in macOS history. Understanding why this happened, what’s different, and how to work with both shells is essential knowledge.

Why Apple Switched

The Licensing Issue

The primary reason was licensing. Bash 3.2 was the last version released under GPLv2. Starting with Bash 4.0 (released in 2009), bash is licensed under GPLv3.

Apple has consistently avoided GPLv3 software because:

  • GPLv3 includes provisions about hardware restrictions (Tivoization)
  • GPLv3 has patent licensing requirements
  • Apple’s business model conflicts with some GPLv3 terms

This left Apple shipping decade-old Bash 3.2:

# macOS's built-in bash (still available)
$ /bin/bash --version
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)
Copyright (C) 2007 Free Software Foundation, Inc.

# Modern bash (via Homebrew)
$ /opt/homebrew/bin/bash --version
GNU bash, version 5.2.26(1)-release (aarch64-apple-darwin23.2.0)

Zsh’s Advantages

Beyond licensing, zsh offers genuine improvements:

  • Better completion system
  • More flexible customization
  • Improved array handling
  • Better globbing and pattern matching
  • More active development community
  • Broad plugin ecosystem (Oh My Zsh, etc.)

What Changed

Default Shell

New user accounts use zsh. Existing accounts retain their shell:

# Check your current shell
$ echo $SHELL
/bin/zsh

# Check what shell is running
$ echo $0
-zsh

# See all configured shells
$ cat /etc/shells

Configuration Files

Zsh uses different configuration files than bash:

BashZshPurpose
~/.bash_profile~/.zprofileLogin shell setup
~/.bashrc~/.zshrcInteractive shell config
~/.bash_login~/.zloginLogin shell (after profile)
~/.bash_logout~/.zlogoutLogout cleanup
-~/.zshenvAll shells (including scripts)

Startup Warnings

If you have bash configurations but use zsh, you may see:

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.

To silence this warning:

# In ~/.bash_profile or ~/.bashrc
export BASH_SILENCE_DEPRECATION_WARNING=1

Staying with Bash

You can continue using bash:

# Change default shell to bash
$ chsh -s /bin/bash

# Or install modern bash and use that
$ brew install bash
$ sudo sh -c 'echo /opt/homebrew/bin/bash >> /etc/shells'
$ chsh -s /opt/homebrew/bin/bash

Note: Using Homebrew’s bash gives you version 5.x features.

Key Differences

Array Indexing

This is a common gotcha:

# Bash: Arrays are 0-indexed
arr=(one two three)
echo ${arr[0]}  # "one"

# Zsh: Arrays are 1-indexed by default
arr=(one two three)
echo ${arr[1]}  # "one"
echo ${arr[0]}  # Empty!

For bash compatibility in zsh:

# In .zshrc - make arrays 0-indexed
setopt KSH_ARRAYS

# Or use zsh-native indexing
echo $arr[1]  # "one"

Word Splitting

# Bash: Variables split on whitespace
var="one two three"
for word in $var; do echo $word; done
# Output: one, two, three (three lines)

# Zsh: Variables don't split by default
var="one two three"
for word in $var; do echo $word; done
# Output: one two three (one line!)

# Zsh: Use explicit splitting
for word in ${=var}; do echo $word; done
# Or set option
setopt SH_WORD_SPLIT

Glob Patterns

# Bash: Unmatched globs pass through as literal
$ ls *.nonexistent
ls: *.nonexistent: No such file or directory

# Zsh: Unmatched globs are errors by default
$ ls *.nonexistent
zsh: no matches found: *.nonexistent

# Zsh: Bash-like behavior
setopt NULL_GLOB  # Unmatched patterns expand to nothing
# or
setopt NO_NOMATCH  # Unmatched patterns pass through literally

History

# Zsh has better history features
# These are often set by default or by Oh My Zsh

# Share history between sessions
setopt SHARE_HISTORY

# Append rather than overwrite
setopt APPEND_HISTORY

# Add timestamps
setopt EXTENDED_HISTORY

# Don't store duplicates
setopt HIST_IGNORE_DUPS

Prompts

# Bash PS1
export PS1='\u@\h:\w\$ '

# Zsh PROMPT (different escape sequences)
export PROMPT='%n@%m:%~%# '

# Zsh equivalents:
# %n = username (\u in bash)
# %m = hostname (\h in bash)
# %~ = current directory with ~ abbreviation (\w in bash)
# %# = # for root, % for users (\$ in bash)

Completion

Zsh’s completion system is more powerful:

# Enable completion system
autoload -Uz compinit
compinit

# Case-insensitive completion
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'

# Menu selection
zstyle ':completion:*' menu select

# Colored completion
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}

Migration Guide

Step 1: Check Current Setup

# What shell files do you have?
ls -la ~/.bash* ~/.zsh* ~/.profile 2>/dev/null

Step 2: Create Zsh Configuration

Create ~/.zshrc with your settings:

# ~/.zshrc

# History configuration
HISTSIZE=10000
SAVEHIST=10000
HISTFILE=~/.zsh_history
setopt SHARE_HISTORY
setopt HIST_IGNORE_DUPS

# Directory navigation
setopt AUTO_CD
setopt AUTO_PUSHD

# Completion
autoload -Uz compinit
compinit

# Prompt (simple example)
PROMPT='%F{green}%n@%m%f:%F{blue}%~%f %# '

# Your aliases
alias ll='ls -la'
alias la='ls -A'

# Your PATH additions
export PATH="/opt/homebrew/bin:$PATH"

Step 3: Migrate Aliases and Functions

Most aliases work identically:

# These work in both bash and zsh
alias grep='grep --color=auto'
alias ll='ls -la'
alias ..='cd ..'

Functions may need adjustment:

# Bash function
function_name() {
    local var="$1"
    # ...
}

# Works in zsh too, but zsh allows:
function function_name {
    local var="$1"
    # ...
}

Step 4: Test Scripts

Scripts should specify their interpreter:

#!/bin/bash
# This script will run in bash regardless of user's default shell

#!/bin/zsh
# This script will run in zsh

#!/bin/sh
# This script runs in POSIX sh (which is bash on macOS)

Bash Compatibility Mode

For scripts that need to work in both:

# In zsh, enable bash-like behavior
emulate -L bash
# or individual options:
setopt BASH_REMATCH
setopt KSH_ARRAYS
setopt SH_WORD_SPLIT

Or create a compatibility shim in .zshrc:

# Source bash configuration if it exists
if [ -f ~/.bashrc ]; then
    emulate -L bash
    source ~/.bashrc
    emulate -L zsh
fi

Oh My Zsh

Many users adopt Oh My Zsh for easier zsh configuration:

# Install Oh My Zsh
$ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

Oh My Zsh provides:

  • Sensible defaults
  • Themes (prompt customization)
  • Plugins (git, docker, etc.)
  • Easier configuration
# Example .zshrc with Oh My Zsh
export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME="robbyrussell"
plugins=(git docker brew macos)
source $ZSH/oh-my-zsh.sh

Alternative Frameworks

  • Prezto: Faster than Oh My Zsh
  • Zinit: Modern plugin manager
  • Antibody: Lightweight plugin manager
  • Plain zsh: No framework needed

Checking Shell Compatibility

Test Scripts

# Test script in different shells
$ bash script.sh
$ zsh script.sh
$ sh script.sh

ShellCheck

# Install shellcheck
$ brew install shellcheck

# Check a script
$ shellcheck script.sh

Portable Scripts

For maximum portability, use POSIX sh:

#!/bin/sh
# POSIX-compliant script

# Use [ ] not [[ ]]
if [ "$var" = "value" ]; then
    echo "Match"
fi

# Use $(command) not `command`
result=$(date +%Y)

# Avoid bash/zsh-specific features

Summary

The bash-to-zsh transition:

AspectRecommendation
New usersUse zsh (default), it’s excellent
Existing bash usersMigrate gradually, zsh is compatible with most setups
ScriptsSpecify interpreter explicitly (#!/bin/bash or #!/bin/zsh)
PortabilityUse #!/bin/sh for maximum compatibility
Bash 4+ features neededInstall modern bash via Homebrew

Key zsh differences to remember:

  • Arrays are 1-indexed (not 0)
  • Variables don’t word-split by default
  • Unmatched globs are errors
  • Different prompt escapes
  • More powerful completion and globbing

Zsh is a capable shell that rewards learning its features. Whether you migrate fully or maintain dual proficiency, understanding both shells makes you effective on macOS.

Terminal.app Deep Dive

Terminal.app is macOS’s built-in terminal emulator. While many power users immediately install iTerm2, Terminal.app is surprisingly capable. Understanding its features helps you work effectively on any Mac, even when you can’t install third-party software.

Finding and Launching Terminal

# Terminal is in Utilities
/Applications/Utilities/Terminal.app

# Quick access:
# - Spotlight: Cmd+Space, type "Terminal"
# - Finder: Go → Utilities → Terminal
# - Launchpad: Other folder → Terminal

Creating Quick Access

Add Terminal to your Dock by dragging it from Applications/Utilities, or create a keyboard shortcut:

System Preferences → Keyboard → Shortcuts → Services

  • Enable “New Terminal at Folder”
  • Enable “New Terminal Tab at Folder”

Now you can right-click any folder and open Terminal there.

Terminal Preferences

Access with Cmd+, or Terminal → Preferences:

General Tab

On startup, open: Choose default behavior

  • New window with profile
  • New window with same profile (continues last session’s appearance)

Shells open with:

  • Default login shell (/bin/zsh)
  • Command (specify custom shell or command)

New tabs open with:

  • Default Working Directory
  • Same Working Directory as current tab

Profiles Tab

Terminal uses “profiles” for appearance and behavior settings. The default profile is “Basic.”

Creating Custom Profiles

  1. Click + to add a new profile
  2. Name it (e.g., “Development”)
  3. Configure settings
  4. Set as default with “Default” button

Profile Settings

Text Tab:

  • Font: Choose monospace font (Menlo, SF Mono, etc.)
  • Font size: Typically 12-14pt
  • Text and bold colors
  • Cursor: Block, underline, or vertical bar
  • Cursor blink
# View current terminal size
$ echo "Columns: $COLUMNS, Rows: $LINES"
Columns: 80, Rows: 24

Window Tab:

  • Window size (columns × rows)
  • Background color/image
  • Blur and transparency
  • Title: What appears in title bar

Shell Tab:

  • Startup command (run on shell start)
  • Ask before closing (prevents accidental closure)
  • Close if shell exited cleanly

Keyboard Tab:

  • Use Option as Meta key (important for Emacs, many CLI tools)
  • Function key behavior

Advanced Tab:

  • Terminal type: xterm-256color (recommended for color support)
  • International encoding: UTF-8
  • Scroll behavior

For a good development experience:

  1. Font: SF Mono or Menlo, 13pt
  2. Window size: 120×35 or larger
  3. Terminal type: xterm-256color
  4. Option as Meta: Enable (for Alt key bindings)
  5. Scroll: Enable “Scroll to bottom on input”

Working with Windows and Tabs

Keyboard Shortcuts

ActionShortcut
New windowCmd+N
New tabCmd+T
Close tab/windowCmd+W
Next tabCmd+Shift+] or Ctrl+Tab
Previous tabCmd+Shift+[ or Ctrl+Shift+Tab
Specific tabCmd+1 through Cmd+9
Split pane (macOS Catalina+)Not built-in (use iTerm2)

Window Groups

Save and restore window arrangements:

  1. Arrange windows as desired
  2. Window → Save Windows as Group
  3. Name the group
  4. Restore: Window → Open Window Group

Tab Bar

Show tab bar even with single tab:

  • View → Show Tab Bar
  • Or: Cmd+Shift+T toggles tab bar visibility

Text Selection and Editing

Selection Shortcuts

ActionMethod
Select wordDouble-click
Select lineTriple-click
Select URLCmd+Double-click
Rectangular selectionHold Option, drag
Extend selectionShift+click

Copy and Paste

# Standard shortcuts
Cmd+C          # Copy
Cmd+V          # Paste
Cmd+Shift+V    # Paste escaped (safe for commands)

Paste Escaped (Cmd+Shift+V): Escapes special characters, preventing accidental command execution when pasting untrusted content.

Quick Look

Select text, press Cmd+Y to Quick Look (preview) URLs or file paths.

Marks and Navigation

Terminal maintains “marks” at each command prompt:

Using Marks

ActionShortcut
Jump to previous markCmd+Up
Jump to next markCmd+Down
Select between marksCmd+Shift+Up/Down
Bookmark current lineCmd+U
Jump to bookmarkCmd+Option+U

Searching

ActionShortcut
FindCmd+F
Find nextCmd+G
Find previousCmd+Shift+G
Use selection for findCmd+E

Colors and Themes

Built-in Profiles

Terminal includes several profiles:

  • Basic (black on white)
  • Grass (green on green)
  • Homebrew (green on black, classic)
  • Novel (warm, paper-like)
  • Ocean (blue theme)
  • Pro (semi-transparent dark)
  • Red Sands (warm red tones)
  • Silver Aerogel (metallic)
  • Solid Colors (various)

Importing Themes

Download .terminal files and double-click to import, or:

  1. Terminal → Preferences → Profiles
  2. Click gear icon → Import
  3. Select .terminal file

Popular theme sources:

Creating Custom Colors

  1. Preferences → Profiles → Text
  2. Click color swatches to customize
  3. For background: Window tab

256-Color Support

Ensure “Declare terminal as” is set to xterm-256color:

# Test 256-color support
$ for i in {0..255}; do printf "\e[48;5;${i}m  \e[0m"; done; echo

Shell Integration

macOS Terminal has built-in shell integration (since El Capitan):

What Shell Integration Provides

  • Marks at each prompt (for navigation)
  • Current directory tracking (new tabs open in same directory)
  • Command exit status indication
  • Notification on long-running command completion

Enabling Shell Integration

Shell integration is automatic with the default zsh configuration. For bash or custom configurations:

# In .zshrc or .bashrc
# This is usually automatic, but can be explicit:
if [[ "$TERM_PROGRAM" == "Apple_Terminal" ]]; then
    # Terminal is managing shell integration
fi

Directory Tracking

With shell integration, Terminal knows your working directory:

# New tab opens in same directory
# Cmd+T → new tab at ~/Documents (if that's where you were)

# Title bar shows current directory

Command Completion Notifications

Terminal can notify you when long commands finish:

  1. Edit → Notify When Running Process Completes
  2. Or: View → Allow Notifications

Touch Bar Support

On MacBook Pro with Touch Bar:

  • Man page viewer
  • Color pickers
  • Autocomplete suggestions

Customize in System Preferences → Keyboard → Customize Touch Bar.

Secure Input

For entering passwords securely:

  1. Terminal → Secure Keyboard Entry (or Cmd+Shift+S)
  2. Prevents other applications from reading keystrokes
  3. Disable when done to allow normal functionality
# Icon in title bar indicates secure input mode

Working with Files

Opening Files

# Open in default application
$ open document.pdf
$ open image.png

# Open in specific application
$ open -a Preview image.png
$ open -a "Visual Studio Code" file.txt

# Reveal in Finder
$ open -R file.txt

Drag and Drop

  • Drag files to Terminal to insert their paths
  • Paths are automatically quoted if they contain spaces

Services

Right-click selected text for Services menu:

  • Open man page
  • Search in Spotlight
  • Look up in Dictionary

Printing

Print terminal content:

  1. Make selection (optional—prints all if nothing selected)
  2. File → Print or Cmd+P
  3. Options: Include timestamp, color, etc.

Accessibility

VoiceOver Support

Terminal works with VoiceOver (screen reader):

  • Cmd+F5 to enable VoiceOver
  • Reads command output
  • Navigate with VoiceOver commands

Font Smoothing

For better readability:

  • System Preferences → Accessibility → Display
  • Adjust contrast and transparency

Window Zoom

  • Cmd++ / Cmd+- to zoom
  • View → Reset Zoom to default

Automation

AppleScript

Terminal is scriptable:

tell application "Terminal"
    do script "echo 'Hello from AppleScript'"
    do script "cd ~/Documents" in front window
end tell

Opening URLs

Terminal recognizes URLs:

  • Cmd+Click to open in browser
  • URLs are underlined when hovering

Custom URL Schemes

Create custom schemes to open Terminal from other apps:

# URL: x-terminal://open?command=ls%20-la

Troubleshooting

Reset Terminal

If settings get corrupted:

# Reset to factory defaults
$ defaults delete com.apple.Terminal
# Restart Terminal

Slow Startup

If Terminal is slow to open:

# Clear ASL logs
$ sudo rm -rf /var/log/asl/*.asl

# Check shell startup files for slow operations
# Time your shell startup:
$ time zsh -i -c exit

TERM Variable Issues

If applications look wrong:

# Check TERM
$ echo $TERM
xterm-256color

# Set in Preferences → Profiles → Advanced
# Or in shell config:
export TERM=xterm-256color

Summary

Terminal.app capabilities:

FeatureAvailable
256 colorsYes
Unicode/UTF-8Yes
TabsYes
Split panesNo (use iTerm2)
Shell integrationYes
ProfilesYes
Touch BarYes
Secure inputYes
AppleScriptYes

Terminal.app is a solid terminal emulator that handles most needs. For advanced features like split panes, triggers, or extensive customization, consider iTerm2. But for many users, Terminal.app is sufficient and has the advantage of requiring no additional installation.

iTerm2 and Alternative Terminals

While Terminal.app is capable, many power users prefer third-party terminal emulators. iTerm2 is the most popular choice, offering features like split panes, extensive customization, and powerful automation. This chapter covers iTerm2 in depth and surveys alternatives.

iTerm2

iTerm2 is a free, open-source terminal emulator for macOS with an active community and extensive feature set.

Installation

# Via Homebrew (recommended)
$ brew install --cask iterm2

# Or download from: https://iterm2.com

Key Features

Split Panes

Create multiple panes in a single tab:

ActionShortcut
Split verticallyCmd+D
Split horizontallyCmd+Shift+D
Navigate panesCmd+Option+Arrow
Maximize paneCmd+Shift+Enter
Close paneCmd+W

Hotkey Window

A terminal that slides down from the top (like Quake consoles):

  1. Preferences → Keys → Hotkey
  2. Create a Dedicated Hotkey Window
  3. Assign a hotkey (e.g., Option+Space)
  4. Configure to show/hide with hotkey

Search with Regex

Cmd+F opens find bar with regex support:

# Search for IP addresses
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}

# Search for email-like patterns
\S+@\S+\.\S+

Shell Integration

iTerm2’s shell integration provides:

  • Current directory in tab title
  • Command history with timestamps
  • Marks at prompts
  • Captured command output
  • Alerts on long command completion
  • File download from remote servers

Install:

# Automatic installation
$ curl -L https://iterm2.com/shell_integration/install_shell_integration.sh | bash

# Or install for specific shell
$ curl -L https://iterm2.com/shell_integration/zsh -o ~/.iterm2_shell_integration.zsh
$ echo 'source ~/.iterm2_shell_integration.zsh' >> ~/.zshrc

After installation, right-click to:

  • Select output of last command
  • Download files from remote hosts
  • Open Recent Directories

Triggers

Automatically perform actions when text matches patterns:

  1. Preferences → Profiles → Advanced → Triggers
  2. Add regex patterns
  3. Choose actions: highlight, alert, run command, etc.

Examples:

  • Highlight errors in red
  • Alert on build completion
  • Run command when pattern matches

Instant Replay

Scroll back through terminal history with time-based replay:

  1. Press Cmd+Option+B
  2. Use arrow keys to move through history
  3. Press Escape to return to live view

Password Manager

Store frequently-used passwords:

  1. Preferences → Profiles → Advanced → Triggers
  2. Or: Window → Password Manager (Cmd+Option+F)
  3. Store username/password pairs
  4. Click to paste (use with Secure Keyboard Entry)

Image Display

iTerm2 can display images inline:

# Using imgcat (part of shell integration)
$ imgcat image.png

# For remote servers, install imgcat:
$ curl -L https://iterm2.com/utilities/imgcat -o ~/bin/imgcat
$ chmod +x ~/bin/imgcat

Broadcast Input

Type in multiple panes simultaneously:

  1. Shell → Broadcast Input
  2. Options: To All Panes, To All Tabs, etc.
  3. Type once, appears everywhere

Useful for running the same command on multiple servers.

Profiles

Create different profiles for different contexts:

  • Development (large font, dark theme)
  • SSH (different colors per server)
  • Presentation (huge font, high contrast)

Switch with Cmd+O (profile picker).

Configuration

Appearance

Preferences → Appearance:

  • Theme: Dark, Light, Minimal, etc.
  • Tab position: Top, Left, Bottom
  • Status bar: Enable/configure

Keys

Preferences → Keys → Key Bindings:

  • Global hotkey
  • Custom key mappings
  • Presets: Natural Text Editing (makes Option+Arrow work like other apps)

Natural Text Editing Preset:

  1. Preferences → Profiles → Keys
  2. Key Mappings → Presets → Natural Text Editing

This enables:

  • Option+Left/Right - Move by word
  • Option+Backspace - Delete word
  • Cmd+Left/Right - Move to line start/end

Status Bar

Preferences → Profiles → Session → Status bar:

Components:

  • Current directory
  • Git branch
  • CPU/Memory usage
  • Battery
  • Custom text

Scripting

iTerm2 supports Python scripting:

#!/usr/bin/env python3
# Requires: pip install iterm2

import iterm2

async def main(connection):
    app = await iterm2.async_get_app(connection)
    window = app.current_terminal_window
    if window:
        await window.current_tab.current_session.async_send_text('echo Hello\n')

iterm2.run_until_complete(main)

For productivity:

  1. Enable hotkey window (Option+Space)
  2. Install shell integration
  3. Enable Natural Text Editing
  4. Configure triggers for errors/completions
  5. Set up profiles for different contexts

Alternative Terminal Emulators

Alacritty

GPU-accelerated, fast, cross-platform terminal:

$ brew install --cask alacritty

Characteristics:

  • Very fast (GPU rendering)
  • Configuration via TOML file
  • No tabs (use tmux)
  • Cross-platform (macOS, Linux, Windows)
  • Minimal features by design

Configuration (~/.config/alacritty/alacritty.toml):

[font]
normal = { family = "SF Mono", style = "Regular" }
size = 13.0

[colors.primary]
background = "#1d1f21"
foreground = "#c5c8c6"

[window]
dimensions = { columns = 120, lines = 35 }
padding = { x = 5, y = 5 }

Best for: Users who want speed and use tmux for session management.

Kitty

Feature-rich, GPU-accelerated terminal:

$ brew install --cask kitty

Characteristics:

  • GPU rendering (fast)
  • Native tabs and splits
  • Image display
  • Ligature support
  • Remote file access
  • Extensive customization

Configuration (~/.config/kitty/kitty.conf):

font_family SF Mono
font_size 13.0
background #1a1a1a
foreground #d8d8d8
enable_audio_bell no

Best for: Users wanting features without sacrificing performance.

Warp

Modern terminal with AI features:

$ brew install --cask warp

Characteristics:

  • Block-based command interface
  • AI command suggestions
  • Modern IDE-like editing
  • Shared workflows
  • Requires account (free tier available)

Best for: Users who want a modern take on terminals with AI assistance.

Hyper

Web-technology-based terminal:

$ brew install --cask hyper

Characteristics:

  • Built on Electron (HTML/CSS/JS)
  • Highly themeable
  • Plugin ecosystem
  • Cross-platform
  • Higher memory usage than native apps

Configuration (~/.hyper.js):

module.exports = {
    config: {
        fontSize: 13,
        fontFamily: '"SF Mono", monospace',
        cursorShape: 'BLOCK',
        shell: '/bin/zsh',
    },
    plugins: ['hyper-dracula'],
};

Best for: Web developers who want to customize with JS/CSS.

WezTerm

GPU-accelerated, cross-platform, Lua-configured:

$ brew install --cask wezterm

Characteristics:

  • GPU rendering
  • Lua configuration
  • Multiplexing built-in
  • Cross-platform
  • Active development

Configuration (~/.wezterm.lua):

local wezterm = require 'wezterm'
return {
    font = wezterm.font 'SF Mono',
    font_size = 13.0,
    color_scheme = 'Catppuccin Mocha',
    enable_tab_bar = true,
}

Best for: Users who want customization with a real programming language.

Feature Comparison

FeatureTerminal.appiTerm2AlacrittyKittyWarp
Split panesNoYesNoYesYes
GPU renderingNoOptionalYesYesYes
TabsYesYesNoYesYes
Shell integrationBasicAdvancedNoYesAdvanced
ProfilesYesYesNoYesYes
Images inlineNoYesYesYesYes
Hotkey windowNoYesNoNoNo
Cross-platformNoNoYesYesNo
ConfigurationGUIGUITOMLconfGUI
PriceFreeFreeFreeFreeFreemium

tmux: Terminal Multiplexer

Regardless of which terminal you choose, tmux provides powerful session management:

$ brew install tmux

tmux provides:

  • Sessions that persist after disconnection
  • Split panes (works in any terminal)
  • Multiple windows
  • Session sharing
  • Scriptable

Basic tmux usage:

# Start new session
$ tmux new -s work

# Detach
Ctrl+b, d

# List sessions
$ tmux ls

# Reattach
$ tmux attach -t work

# Split panes
Ctrl+b, %    # Vertical
Ctrl+b, "    # Horizontal

# Navigate panes
Ctrl+b, arrow

Recommendation: If you use Alacritty, tmux is essential. For iTerm2 or Kitty users, tmux is optional but useful for remote work.

Choosing a Terminal

Use Terminal.app if:

  • You want no additional software
  • You’re working on shared/managed Macs
  • Your needs are basic

Use iTerm2 if:

  • You want split panes and hotkey window
  • You need advanced features (triggers, shell integration)
  • You prefer GUI configuration
  • You’re a power user

Use Alacritty/Kitty if:

  • You want maximum performance
  • You prefer config-file configuration
  • You use tmux
  • You work cross-platform

Use Warp if:

  • You want AI assistance
  • You prefer modern IDE-like interfaces
  • You’re comfortable with an account requirement

Summary

The terminal emulator choice is personal, but common paths:

  1. Start with Terminal.app - Learn the basics
  2. Graduate to iTerm2 - Most macOS power users land here
  3. Consider alternatives - If you have specific needs (speed, cross-platform, etc.)

iTerm2’s combination of features, stability, and active development makes it the default recommendation for macOS power users. But any terminal that supports UTF-8 and 256 colors will work well for most tasks—the choice is about workflow optimization, not capability.

Shell Configuration on macOS

Getting your shell configured correctly is fundamental to a productive command-line experience. This chapter explains which configuration files are loaded when, how to structure your setup, and macOS-specific considerations.

Understanding Shell Invocation Modes

Shells can be invoked in different modes, and different configuration files are loaded depending on the mode:

Login vs Non-Login Shell

Login shell: First shell started when you log in. Characteristics:

  • Reads “profile” files
  • Sets up initial environment
  • Terminal.app starts login shells by default

Non-login shell: Shells started from other shells or programs:

  • Reads “rc” (run commands) files
  • Inherits environment from parent
  • Scripts typically run as non-login shells

Interactive vs Non-Interactive

Interactive: You’re typing commands at a prompt:

  • Reads configuration files
  • Sets up completion, prompts, aliases

Non-interactive: Running a script:

  • Generally skips most configuration
  • Faster startup

Zsh Configuration Files

Zsh reads files in this order:

All Shells

  1. /etc/zshenv - System-wide, all shells
  2. ~/.zshenv - User, all shells (including scripts)

Login Shells

  1. /etc/zprofile - System-wide login
  2. ~/.zprofile - User login

Interactive Shells

  1. /etc/zshrc - System-wide interactive
  2. ~/.zshrc - User interactive

Login Shell Completion

  1. /etc/zlogin - System-wide, after zshrc
  2. ~/.zlogin - User, after zshrc

Logout

  1. ~/.zlogout - User logout cleanup
  2. /etc/zlogout - System-wide logout

Diagram

                    ┌──────────────────┐
                    │  Shell Started   │
                    └────────┬─────────┘
                             │
                    ┌────────┴─────────┐
                    │    .zshenv       │ ← All zsh instances
                    └────────┬─────────┘
                             │
            ┌────────────────┼────────────────┐
            │                │                │
      ┌─────┴──────┐   ┌─────┴──────┐  ┌─────┴──────┐
      │ Non-Login  │   │   Login    │  │   Script   │
      │Interactive │   │Interactive │  │    (sh)    │
      └─────┬──────┘   └─────┬──────┘  └────────────┘
            │                │
            │         ┌──────┴──────┐
            │         │  .zprofile  │
            │         └──────┬──────┘
            │                │
      ┌─────┴────────────────┴──────┐
      │          .zshrc             │ ← Interactive shells
      └─────┬────────────────┬──────┘
            │                │
            │         ┌──────┴──────┐
            │         │   .zlogin   │
            │         └─────────────┘
            │
       (shell runs)
FileUse For
~/.zshenvEnvironment variables needed by scripts (rarely modified)
~/.zprofileLogin-specific setup (PATH modifications for login shells)
~/.zshrcInteractive configuration (aliases, prompts, completion, key bindings)
~/.zloginCommands that should run after zshrc (rarely used)
~/.zlogoutCleanup on logout (rarely used)

Bash Configuration Files

For those still using bash:

Login Shells

  1. /etc/profile - System-wide
  2. First found of: ~/.bash_profile, ~/.bash_login, ~/.profile

Non-Login Interactive Shells

  1. /etc/bash.bashrc (not on macOS by default)
  2. ~/.bashrc

macOS Quirk

Terminal.app always starts login shells, but many expect .bashrc to be read. Common solution:

# In ~/.bash_profile
if [ -f ~/.bashrc ]; then
    source ~/.bashrc
fi

Example Zsh Configuration

~/.zshenv

Keep minimal—runs for all shells including scripts:

# ~/.zshenv
# Only put things here that ALL zsh instances need

# Skip global compinit for faster startup
skip_global_compinit=1

~/.zprofile

Login-specific PATH modifications:

# ~/.zprofile
# Runs for login shells (Terminal.app)

# Add Homebrew to PATH (Apple Silicon)
eval "$(/opt/homebrew/bin/brew shellenv)"

# Or Intel Mac
# eval "$(/usr/local/bin/brew shellenv)"

~/.zshrc

The main configuration file:

# ~/.zshrc
# Interactive shell configuration

#------------------
# History
#------------------
HISTSIZE=50000
SAVEHIST=50000
HISTFILE=~/.zsh_history

setopt EXTENDED_HISTORY       # Save timestamp
setopt HIST_EXPIRE_DUPS_FIRST # Expire duplicates first
setopt HIST_IGNORE_DUPS       # Don't store duplicates
setopt HIST_IGNORE_SPACE      # Ignore commands starting with space
setopt HIST_VERIFY            # Show expanded history before executing
setopt SHARE_HISTORY          # Share between sessions

#------------------
# Directory Navigation
#------------------
setopt AUTO_CD           # Type directory name to cd
setopt AUTO_PUSHD        # Push directories to stack
setopt PUSHD_IGNORE_DUPS # Ignore duplicate directories
setopt PUSHD_SILENT      # Silent pushd

#------------------
# Completion
#------------------
autoload -Uz compinit
compinit

# Case-insensitive completion
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'

# Menu selection
zstyle ':completion:*' menu select

# Verbose completion
zstyle ':completion:*' verbose yes

# Group completions by category
zstyle ':completion:*' group-name ''

#------------------
# Key Bindings
#------------------
bindkey -e  # Emacs key bindings (default)
# Or: bindkey -v for vi mode

# Better history search
bindkey '^[[A' history-search-backward  # Up arrow
bindkey '^[[B' history-search-forward   # Down arrow

# Word navigation (Option+Arrow)
bindkey "^[[1;3C" forward-word   # Option+Right
bindkey "^[[1;3D" backward-word  # Option+Left

# Delete word
bindkey "^[^?" backward-kill-word  # Option+Backspace

#------------------
# Prompt
#------------------
# Simple prompt with git info
autoload -Uz vcs_info
precmd_vcs_info() { vcs_info }
precmd_functions+=( precmd_vcs_info )
setopt prompt_subst
zstyle ':vcs_info:git:*' formats ' (%b)'
PROMPT='%F{green}%n@%m%f:%F{blue}%~%f%F{yellow}${vcs_info_msg_0_}%f %# '

# Right-side prompt (optional)
# RPROMPT='%F{gray}%T%f'  # Time

#------------------
# Aliases
#------------------
alias ll='ls -la'
alias la='ls -A'
alias l='ls -CF'
alias grep='grep --color=auto'
alias ..='cd ..'
alias ...='cd ../..'

# macOS specific
alias o='open'
alias finder='open -a Finder .'

#------------------
# Functions
#------------------
# Create directory and cd into it
mkcd() {
    mkdir -p "$1" && cd "$1"
}

# Extract various archive types
extract() {
    if [ -f "$1" ]; then
        case "$1" in
            *.tar.bz2) tar xjf "$1" ;;
            *.tar.gz)  tar xzf "$1" ;;
            *.bz2)     bunzip2 "$1" ;;
            *.gz)      gunzip "$1" ;;
            *.tar)     tar xf "$1" ;;
            *.tbz2)    tar xjf "$1" ;;
            *.tgz)     tar xzf "$1" ;;
            *.zip)     unzip "$1" ;;
            *.Z)       uncompress "$1" ;;
            *.7z)      7z x "$1" ;;
            *)         echo "'$1' cannot be extracted" ;;
        esac
    else
        echo "'$1' is not a file"
    fi
}

#------------------
# Tool Configuration
#------------------
# FZF (if installed)
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh

# Node Version Manager (if installed)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"

# Python environment
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
command -v pyenv >/dev/null && eval "$(pyenv init -)"

#------------------
# Local Configuration
#------------------
# Machine-specific settings not in version control
[ -f ~/.zshrc.local ] && source ~/.zshrc.local

macOS-Specific Configuration

Homebrew Setup

# In ~/.zprofile for login shells, or ~/.zshrc

# Apple Silicon
if [[ -f /opt/homebrew/bin/brew ]]; then
    eval "$(/opt/homebrew/bin/brew shellenv)"
fi

# Intel
if [[ -f /usr/local/bin/brew ]]; then
    eval "$(/usr/local/bin/brew shellenv)"
fi

macOS Aliases

# Quick Look
ql() { qlmanage -p "$@" &>/dev/null; }

# Show/hide hidden files
alias showfiles='defaults write com.apple.finder AppleShowAllFiles YES; killall Finder'
alias hidefiles='defaults write com.apple.finder AppleShowAllFiles NO; killall Finder'

# Clipboard
alias pbp='pbpaste'
alias pbc='pbcopy'

# Empty trash
alias emptytrash='rm -rf ~/.Trash/*'

# Lock screen
alias lock='/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend'

# Get macOS software updates
alias update='sudo softwareupdate -i -a'

PATH Configuration

Common PATH additions:

# In ~/.zprofile or ~/.zshrc

# Standard local binaries
export PATH="/usr/local/bin:$PATH"

# Homebrew (Apple Silicon)
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"

# User binaries
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"

# Development tools
export PATH="$HOME/go/bin:$PATH"              # Go
export PATH="$HOME/.cargo/bin:$PATH"           # Rust
export PATH="$HOME/.rbenv/bin:$PATH"           # Ruby

Configuration Management

Version Control

Keep your dotfiles in Git:

# Initialize dotfiles repo
$ cd ~
$ git init --bare ~/.dotfiles

# Create alias
alias dotfiles='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'

# Add files
$ dotfiles add ~/.zshrc ~/.zprofile
$ dotfiles commit -m "Add shell config"

# Push to remote
$ dotfiles remote add origin git@github.com:username/dotfiles.git
$ dotfiles push -u origin main

Modular Configuration

Split configuration into separate files:

# In ~/.zshrc
# Load all configuration modules
for config in ~/.config/zsh/*.zsh; do
    source "$config"
done
~/.config/zsh/
├── aliases.zsh
├── completion.zsh
├── functions.zsh
├── history.zsh
├── keybindings.zsh
└── prompt.zsh

Local Overrides

Keep machine-specific settings separate:

# End of ~/.zshrc
# Load local configuration (not version controlled)
[ -f ~/.zshrc.local ] && source ~/.zshrc.local

Add to .gitignore:

.zshrc.local

Debugging Configuration

Startup Time

Profile shell startup:

# Time shell startup
$ time zsh -i -c exit
zsh -i -c exit  0.08s user 0.04s system 94% cpu 0.124 total

# Detailed profiling
$ zsh -xv 2>&1 | head -100

Trace Loading

# In ~/.zshrc, at the beginning:
zmodload zsh/zprof

# At the end:
zprof

Finding Slow Operations

Common slowdowns:

  • compinit without caching
  • nvm/rbenv initialization
  • Plugin managers loading many plugins
  • Network operations in prompts

Solutions:

# Cache compinit
autoload -Uz compinit
if [[ -n ${ZDOTDIR}/.zcompdump(#qN.mh+24) ]]; then
    compinit
else
    compinit -C
fi

# Lazy-load slow tools
nvm() {
    unfunction nvm
    export NVM_DIR="$HOME/.nvm"
    source "$NVM_DIR/nvm.sh"
    nvm "$@"
}

Summary

Shell configuration on macOS:

ShellMain ConfigLogin Config
zsh~/.zshrc~/.zprofile
bash~/.bashrc~/.bash_profile

Best practices:

  1. Keep .zshenv minimal — runs for all shells
  2. Put PATH in .zprofile — for login shells
  3. Put interactive config in .zshrc — aliases, prompts, completion
  4. Use .zshrc.local — for machine-specific settings
  5. Version control your dotfiles — but not secrets
  6. Profile startup time — optimize if needed

The configuration files structure your shell experience. Take time to set them up well, and your command-line work becomes significantly more efficient.

Environment Variables and PATH

Environment variables on macOS present unique challenges. Between Homebrew, GUI applications, multiple shell contexts, and macOS-specific mechanisms like launchd, getting your PATH and environment right requires understanding how each context inherits (or doesn’t inherit) environment settings.

Environment Variable Basics

Environment variables are name-value pairs available to processes:

# View all environment variables
$ env
# or
$ printenv

# View specific variable
$ echo $HOME
/Users/david

# Set variable for current session
$ export MY_VAR="value"

# Set for a single command
$ MY_VAR="value" some_command

Important Built-in Variables

VariablePurposeExample
PATHExecutable search path/usr/bin:/bin
HOMEUser home directory/Users/david
USERCurrent usernamedavid
SHELLDefault shell/bin/zsh
TERMTerminal typexterm-256color
LANGLocale settingen_US.UTF-8
EDITORDefault text editorvim
TMPDIRTemporary directory/var/folders/.../T/
PWDCurrent working directory/Users/david/project

macOS-Specific Variables

VariablePurpose
TERM_PROGRAMTerminal app name (Apple_Terminal, iTerm.app)
TERM_SESSION_IDUnique session identifier
__CFBundleIdentifierCurrent app bundle ID
COMMAND_MODEUnix compatibility mode
SECURITYSESSIONIDSecurity session

Understanding PATH

PATH tells the shell where to find executables:

$ echo $PATH
/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

# Shell searches directories left to right
# First match wins

Default macOS PATH

A fresh macOS installation has a basic PATH:

/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

PATH Sources

macOS PATH comes from multiple places:

  1. System-wide PATH (/etc/paths):
$ cat /etc/paths
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin
  1. PATH additions (/etc/paths.d/*):
$ ls /etc/paths.d
100-rvictl
MacGPG2

$ cat /etc/paths.d/MacGPG2
/usr/local/MacGPG2/bin
  1. Shell configuration (~/.zprofile, ~/.zshrc):
export PATH="/opt/homebrew/bin:$PATH"
  1. path_helper utility (/usr/libexec/path_helper):
# Combines /etc/paths and /etc/paths.d
$ /usr/libexec/path_helper -s
PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/MacGPG2/bin"; export PATH;

Setting PATH Correctly

For zsh (default shell):

# In ~/.zprofile (for login shells)
# Add to BEGINNING of PATH (searched first)
export PATH="/opt/homebrew/bin:$PATH"

# Add to END of PATH (searched last)
export PATH="$PATH:$HOME/bin"

Common PATH Additions

# Homebrew (Apple Silicon)
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"

# Homebrew (Intel)
export PATH="/usr/local/bin:/usr/local/sbin:$PATH"

# User binaries
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"

# Go
export PATH="$HOME/go/bin:$PATH"

# Rust
export PATH="$HOME/.cargo/bin:$PATH"

# Python (pyenv)
export PATH="$HOME/.pyenv/shims:$PATH"

# Node (nvm adds its own path)

Environment for GUI Applications

GUI applications do not inherit your shell environment. This is a common source of confusion.

The Problem

# Terminal: This works
$ echo $MY_VAR
my_value

# But GUI apps don't see it
# VS Code launched from Dock won't have MY_VAR

Solutions

1. Launch from Terminal

# Open VS Code with terminal's environment
$ code .

# Open any app
$ open -a "Visual Studio Code"

2. Use launchctl setenv

Set environment variables system-wide:

# Set for current boot
$ launchctl setenv MY_VAR "my_value"

# Verify
$ launchctl getenv MY_VAR
my_value

Note: This persists only until logout. For persistence, use Launch Agents.

3. Launch Agent for Persistent Environment

Create ~/Library/LaunchAgents/environment.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>my.environment</string>
    <key>ProgramArguments</key>
    <array>
        <string>sh</string>
        <string>-c</string>
        <string>
            launchctl setenv MY_VAR "my_value"
            launchctl setenv PATH "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
        </string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

Load it:

$ launchctl load ~/Library/LaunchAgents/environment.plist

4. Per-Application Settings

Some apps have their own environment settings:

  • VS Code: terminal.integrated.env.osx in settings
  • IntelliJ: Run configurations → Environment variables
  • Xcode: Scheme → Run → Arguments → Environment Variables

Checking GUI App Environment

# See what environment an app sees
$ open -a TextEdit
# In TextEdit, open: /dev/fd/1 shows stdout
# Or check in Activity Monitor → Process → Environment

SSH and Remote Sessions

SSH sessions need environment setup too:

~/.ssh/environment

If PermitUserEnvironment is enabled on the server:

# ~/.ssh/environment
MY_VAR=value
PATH=/usr/local/bin:/usr/bin:/bin

SendEnv and AcceptEnv

Configure which variables are sent over SSH:

# ~/.ssh/config
Host *
    SendEnv LANG LC_*
    SendEnv MY_*

Server must have AcceptEnv configured to receive them.

Remote PATH Issues

Remote servers won’t have your local Homebrew:

# Script that works locally may fail remotely
$ ssh server 'brew list'  # Fails - brew not in default PATH

# Solutions:
# 1. Use full path
$ ssh server '/opt/homebrew/bin/brew list'

# 2. Source profile
$ ssh server 'source ~/.profile && brew list'

# 3. Interactive shell
$ ssh -t server 'bash -l -c "brew list"'

Common Issues and Solutions

“Command Not Found” After Installing

$ brew install something
$ something
zsh: command not found: something

Solutions:

# Refresh PATH
$ source ~/.zprofile
# Or
$ source ~/.zshrc
# Or start a new terminal

# Check if Homebrew PATH is set
$ echo $PATH | grep homebrew
# Should see /opt/homebrew/bin

# If not, add to ~/.zprofile:
eval "$(/opt/homebrew/bin/brew shellenv)"

PATH Order Issues

# System Python instead of Homebrew Python
$ which python3
/usr/bin/python3  # Wrong!

# Check PATH order
$ echo $PATH | tr ':' '\n'

Fix: Ensure Homebrew path comes before system paths:

export PATH="/opt/homebrew/bin:$PATH"

Subshell Doesn’t Have Variables

$ export MY_VAR="value"
$ bash -c 'echo $MY_VAR'  # Works
$ zsh script.zsh          # May not work if script starts fresh

Solution: Use -l for login shell or source configuration:

$ bash -l -c 'echo $MY_VAR'

Scripts Don’t Have PATH

Scripts run with #!/bin/sh start with minimal environment:

#!/bin/sh
brew list  # May fail - brew not in PATH

Solutions:

# Use full path in scripts
#!/bin/sh
/opt/homebrew/bin/brew list

# Or set PATH in script
#!/bin/sh
export PATH="/opt/homebrew/bin:$PATH"
brew list

# Or use env with explicit path
#!/usr/bin/env -P /opt/homebrew/bin python3

Environment File Patterns

Per-Directory Environment (.env files)

Many tools support .env files:

# .env file
DATABASE_URL=postgres://localhost/mydb
API_KEY=secret123

Load manually:

$ export $(grep -v '^#' .env | xargs)

Or use tools like direnv:

$ brew install direnv
$ echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc

# Now .envrc files load automatically

direnv for Automatic Environment

# Install
$ brew install direnv

# Add to ~/.zshrc
eval "$(direnv hook zsh)"

# Create .envrc in project
$ cd myproject
$ echo 'export MY_VAR=value' > .envrc
$ direnv allow

# Variables load when you enter directory
$ cd myproject
direnv: loading .envrc
direnv: export +MY_VAR

$ cd ..
direnv: unloading

Summary

Environment variable contexts on macOS:

ContextConfig LocationInheritance
Login shell~/.zprofileFrom system
Interactive shell~/.zshrcFrom login shell
ScriptsMinimalMust be explicit
GUI appsNonelaunchctl setenv
SSH~/.ssh/environmentExplicit
Subprocesses-From parent

Key points:

  1. PATH order matters — first match wins
  2. GUI apps don’t get shell environment — use launchctl or launch from terminal
  3. Homebrew needs explicit PATH setup — add in .zprofile
  4. Scripts have minimal environment — use full paths or set PATH explicitly
  5. Use direnv for project-specific environment — automatic loading/unloading

Getting environment configuration right eliminates entire categories of “works on my machine” problems.

macOS-Specific Shell Features

macOS provides unique commands and capabilities that integrate the command line with the graphical system. These tools let you leverage macOS features—clipboard, Spotlight, text-to-speech, notifications—from your shell scripts and daily workflow.

Clipboard Integration

pbcopy and pbpaste

The pasteboard (clipboard) is accessible from the command line:

# Copy to clipboard
$ echo "Hello" | pbcopy

# Paste from clipboard
$ pbpaste
Hello

# Copy file contents
$ cat file.txt | pbcopy
# or
$ pbcopy < file.txt

# Paste to file
$ pbpaste > output.txt

Practical Uses

# Copy current directory path
$ pwd | pbcopy

# Copy file contents to clipboard
$ cat ~/.ssh/id_rsa.pub | pbcopy
echo "SSH key copied to clipboard"

# Generate and copy a password
$ openssl rand -base64 12 | pbcopy

# Copy command output
$ ls -la | pbcopy

# Pipe clipboard through a command
$ pbpaste | sort | uniq | pbcopy

# Convert clipboard contents
$ pbpaste | tr '[:lower:]' '[:upper:]' | pbcopy

Multiple Pasteboards

macOS has multiple pasteboards:

# General pasteboard (default)
$ pbcopy
$ pbpaste

# Find pasteboard (Cmd+E in many apps)
$ pbcopy -pboard find
$ pbpaste -pboard find

# Ruler pasteboard
$ pbcopy -pboard ruler

The open Command

open is one of the most versatile macOS commands:

Basic Usage

# Open file with default application
$ open document.pdf      # Opens in Preview
$ open image.png         # Opens in Preview
$ open file.html         # Opens in Safari

# Open directory in Finder
$ open .                 # Current directory
$ open ~/Documents       # Specific directory

# Open URL
$ open https://apple.com

Specifying Applications

# Open with specific application
$ open -a Safari https://google.com
$ open -a "Visual Studio Code" file.txt
$ open -a Preview *.png

# Open with application bundle identifier
$ open -b com.apple.Safari https://apple.com

Additional Options

# Open new instance of application
$ open -n -a Safari

# Reveal file in Finder (don't open it)
$ open -R file.txt

# Open in background
$ open -g document.pdf

# Wait for application to close before returning
$ open -W document.pdf

# Edit with TextEdit
$ open -e file.txt

# Read from stdin
$ ls -la | open -f    # Opens output in TextEdit

Opening System Preferences

# Open System Preferences
$ open "x-apple.systempreferences:"

# Open specific preference pane
$ open "x-apple.systempreferences:com.apple.preference.security"
$ open "x-apple.systempreferences:com.apple.preference.network"
$ open "x-apple.systempreferences:com.apple.preferences.Bluetooth"

# Apple Silicon: System Settings uses different IDs
$ open "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension"

Spotlight from Command Line

mdfind

Search using Spotlight:

# Basic search
$ mdfind "search term"

# Search by filename
$ mdfind -name "document.pdf"

# Search in specific directory
$ mdfind -onlyin ~/Documents "budget"

# Search by file type
$ mdfind "kMDItemContentType == 'public.jpeg'"

# Search by date
$ mdfind "kMDItemLastUsedDate > $time.today(-7)"

# Live query (continues watching)
$ mdfind -live "kMDItemContentType == 'public.jpeg'"

Common Spotlight Attributes

# Find all PDFs
$ mdfind "kMDItemContentType == 'com.adobe.pdf'"

# Find images
$ mdfind "kMDItemContentType == 'public.image'"

# Find by author
$ mdfind "kMDItemAuthors == 'John Smith'"

# Find large files
$ mdfind "kMDItemFSSize > 100000000"  # > 100MB

# Find files modified today
$ mdfind "kMDItemFSContentChangeDate > $time.today"

# Find apps
$ mdfind "kMDItemContentType == 'com.apple.application-bundle'"

mdls - Spotlight Metadata

View metadata for a file:

$ mdls document.pdf
kMDItemContentType             = "com.adobe.pdf"
kMDItemContentTypeTree         = (
    "com.adobe.pdf",
    "public.data",
    "public.item",
    "public.content"
)
kMDItemDisplayName             = "document.pdf"
kMDItemFSContentChangeDate     = 2024-01-15 10:30:00 +0000
kMDItemFSCreationDate          = 2024-01-10 08:00:00 +0000
kMDItemFSName                  = "document.pdf"
kMDItemFSSize                  = 1048576
...

# Get specific attribute
$ mdls -name kMDItemFSSize document.pdf
kMDItemFSSize = 1048576

mdutil - Spotlight Indexing

Control Spotlight indexing:

# Check indexing status
$ mdutil -s /
/:
    Indexing enabled.

# Disable indexing for volume
$ sudo mdutil -i off /Volumes/External

# Erase and rebuild index
$ sudo mdutil -E /

# Enable indexing
$ sudo mdutil -i on /

Text-to-Speech

say Command

# Basic speech
$ say "Hello, world"

# Specify voice
$ say -v Samantha "Hello"
$ say -v Daniel "Hello"  # British
$ say -v Kyoko "こんにちは"  # Japanese

# List available voices
$ say -v '?'

# Speak file contents
$ say -f document.txt

# Save to audio file
$ say "Hello" -o hello.aiff
$ say "Hello" -o hello.m4a --data-format=aac

# Speak at different rate (words per minute)
$ say -r 200 "Fast speech"
$ say -r 100 "Slow speech"

Practical Uses

# Notify when long command finishes
$ make build; say "Build complete"

# Read log file
$ tail -f /var/log/system.log | while read line; do say "$line"; done

# Speak clipboard
$ pbpaste | say

# Alarm
$ sleep 300; say "Time's up"

Screen and Window Management

screencapture

Take screenshots from command line:

# Capture entire screen
$ screencapture screenshot.png

# Capture selection (interactive)
$ screencapture -i screenshot.png

# Capture specific window (click to select)
$ screencapture -W screenshot.png

# Capture with delay (seconds)
$ screencapture -T 5 screenshot.png

# Capture to clipboard
$ screencapture -c

# Capture without shadow
$ screencapture -o screenshot.png

# Capture in different format
$ screencapture -t jpg screenshot.jpg
$ screencapture -t pdf screenshot.pdf

screen Recording

# Basic screen recording (stop with Ctrl+C)
$ screencapture -v recording.mov

# Record specific display
$ screencapture -D 1 -v recording.mov

# Record specific area
$ screencapture -R 0,0,800,600 -v recording.mov

Notifications

osascript for Notifications

# Display notification
$ osascript -e 'display notification "Hello" with title "My Script"'

# With subtitle and sound
$ osascript -e 'display notification "Task complete" with title "Build" subtitle "Project X" sound name "Glass"'

terminal-notifier (Third-party)

# Install
$ brew install terminal-notifier

# Use
$ terminal-notifier -message "Hello" -title "My Script"

# With action
$ terminal-notifier -message "Click to open" -title "Alert" -open "https://apple.com"

# With sound
$ terminal-notifier -message "Done" -title "Build" -sound default

System Information

sw_vers

macOS version information:

$ sw_vers
ProductName:		macOS
ProductVersion:		14.0
BuildVersion:		23A344

system_profiler

Detailed system information:

# Full report (very verbose)
$ system_profiler

# Specific data types
$ system_profiler SPHardwareDataType
$ system_profiler SPSoftwareDataType
$ system_profiler SPNetworkDataType
$ system_profiler SPStorageDataType

# List available data types
$ system_profiler -listDataTypes

# Machine-readable output
$ system_profiler -json SPHardwareDataType

sysctl

Kernel parameters:

# CPU info
$ sysctl -n machdep.cpu.brand_string
Apple M1 Pro

# Memory size
$ sysctl -n hw.memsize
17179869184

# Number of CPUs
$ sysctl -n hw.ncpu
10

# Kernel version
$ sysctl -n kern.osrelease
23.0.0

Power Management

pmset

Power management settings:

# View current settings
$ pmset -g
System-wide power settings:
Currently in use:
 standby              1
 Sleep On Power Button 1
 hibernatefile        /var/vm/sleepimage
...

# Prevent sleep for a command
$ caffeinate -i long_running_command

# Prevent sleep for duration (seconds)
$ caffeinate -t 3600

# Prevent sleep until process exits
$ caffeinate -w $(pgrep -f "my_process")

# Schedule sleep
$ sudo pmset schedule sleep "01/20/24 22:00:00"

# Schedule wake
$ sudo pmset schedule wake "01/20/24 06:00:00"

User Management

dscl (Directory Service command line)

# List users
$ dscl . -list /Users

# User info
$ dscl . -read /Users/david

# Check group membership
$ dscl . -read /Groups/admin GroupMembership

# List groups
$ dscl . -list /Groups

id

$ id
uid=501(david) gid=20(staff) groups=20(staff),12(everyone),...

$ id david
$ groups david

Networking Commands

networksetup

# List network services
$ networksetup -listallnetworkservices
An asterisk (*) denotes that a network service is disabled.
Wi-Fi
Thunderbolt Ethernet

# Get current Wi-Fi network
$ networksetup -getairportnetwork en0
Current Wi-Fi Network: MyNetwork

# Get IP address
$ networksetup -getinfo "Wi-Fi"

airport

# Scan for networks
$ /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s

# Current connection info
$ /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I

# Create alias for convenience
alias airport='/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'

Disk Commands

# List disks
$ diskutil list

# Disk info
$ diskutil info disk0

# Eject disk
$ diskutil eject /Volumes/External

# Mounting/unmounting
$ diskutil mount disk2s1
$ diskutil unmount /Volumes/External

Summary

macOS-specific shell commands:

CommandPurpose
pbcopy/pbpasteClipboard access
openOpen files/URLs/apps
mdfindSpotlight search
mdlsView file metadata
sayText-to-speech
screencaptureScreenshots/recordings
osascriptRun AppleScript
sw_versmacOS version
system_profilerSystem information
pmsetPower management
networksetupNetwork configuration
diskutilDisk management

These commands bridge the gap between command-line efficiency and macOS’s graphical features, enabling powerful workflows that combine the best of both worlds.

Shell Integration with macOS Services

The command line on macOS doesn’t exist in isolation—it can interact with Finder, Spotlight, Keychain, and other system services. This integration enables workflows that combine the precision of shell commands with the convenience of macOS features.

Finder Integration

Opening Finder from Terminal

# Open current directory
$ open .

# Open specific directory
$ open ~/Documents

# Reveal file in Finder
$ open -R /path/to/file.txt

Getting Finder Selection

Get the currently selected files in Finder:

# Using AppleScript
$ osascript -e 'tell application "Finder" to get selection as alias list'

# More useful: get paths
$ osascript -e 'tell application "Finder"
    set sel to selection as alias list
    set output to ""
    repeat with f in sel
        set output to output & POSIX path of f & "\n"
    end repeat
    return output
end tell'

Create a shell function:

# Add to ~/.zshrc
finder_selection() {
    osascript -e 'tell application "Finder"
        set sel to selection as alias list
        set output to ""
        repeat with f in sel
            set output to output & POSIX path of f & "\n"
        end repeat
        return output
    end tell' 2>/dev/null | sed '/^$/d'
}

# Usage
$ finder_selection
/Users/david/Documents/file1.txt
/Users/david/Documents/file2.txt

Creating Finder Aliases

# Create Finder alias (different from symlink)
$ osascript -e 'tell application "Finder" to make new alias file at desktop to POSIX file "/path/to/original"'

Tags and Comments

# Get tags (using mdls)
$ mdls -name kMDItemUserTags file.txt
kMDItemUserTags = (
    Red,
    Important
)

# Set tags (using xattr - complex, better to use tools)
# Install 'tag' for easier tag management
$ brew install tag

# Add tag
$ tag -a Red file.txt

# Remove tag
$ tag -r Red file.txt

# List tags
$ tag -l file.txt

# Find files with tag
$ tag -f Red
$ mdfind "kMDItemUserTags == 'Red'"

Finder Comments

# Set Finder comment
$ osascript -e 'tell application "Finder" to set comment of (POSIX file "/path/to/file" as alias) to "My comment"'

# Get Finder comment
$ mdls -name kMDItemFinderComment file.txt

Keychain Access

security Command

Access Keychain from the command line:

# Find a password
$ security find-generic-password -a "account_name" -s "service_name" -w
# -w prints just the password

# Find internet password
$ security find-internet-password -a "username" -s "server.com" -w

# Add a password
$ security add-generic-password -a "account_name" -s "service_name" -w "password123"

# Delete a password
$ security delete-generic-password -a "account_name" -s "service_name"

Practical Keychain Usage

Store and retrieve secrets safely:

# Store API key
$ security add-generic-password -a "$USER" -s "my_api_key" -w "secret123"

# Retrieve in scripts
API_KEY=$(security find-generic-password -a "$USER" -s "my_api_key" -w 2>/dev/null)

# Use in environment (in ~/.zshrc.local - not version controlled)
export GITHUB_TOKEN=$(security find-generic-password -a "$USER" -s "github_token" -w 2>/dev/null)

SSH Keys in Keychain

macOS can store SSH passphrases in Keychain:

# Add SSH key to agent with Keychain storage
$ ssh-add --apple-use-keychain ~/.ssh/id_ed25519

# Configure SSH to use Keychain (~/.ssh/config)
Host *
    AddKeysToAgent yes
    UseKeychain yes
    IdentityFile ~/.ssh/id_ed25519

AppleScript and JavaScript for Automation

osascript Command

Run AppleScript from the shell:

# Basic dialog
$ osascript -e 'display dialog "Hello" buttons {"OK"}'

# Get input
$ osascript -e 'text returned of (display dialog "Enter name:" default answer "")'

# Alert
$ osascript -e 'display alert "Warning" message "Something happened"'

# Choose from list
$ osascript -e 'choose from list {"Option 1", "Option 2", "Option 3"}'

JavaScript for Automation (JXA)

Modern alternative to AppleScript:

# Using JXA
$ osascript -l JavaScript -e 'Application("Finder").selection().map(f => f.url())'

# Multi-line JXA
$ osascript -l JavaScript << 'EOF'
const app = Application.currentApplication();
app.includeStandardAdditions = true;
const result = app.displayDialog("Enter your name:", {
    defaultAnswer: "",
    withTitle: "Input"
});
result.textReturned;
EOF

Controlling Applications

# Control iTunes/Music
$ osascript -e 'tell application "Music" to playpause'
$ osascript -e 'tell application "Music" to next track'
$ osascript -e 'tell application "Music" to get name of current track'

# Control Safari
$ osascript -e 'tell application "Safari" to get URL of current tab of window 1'

# Control System Events
$ osascript -e 'tell application "System Events" to get name of every process'

Automator and Shortcuts

Running Automator Workflows

# Run workflow
$ automator /path/to/workflow.workflow

# Run workflow with input
$ automator -i "input text" /path/to/workflow.workflow

Shortcuts (macOS Monterey+)

# List shortcuts
$ shortcuts list

# Run a shortcut
$ shortcuts run "My Shortcut"

# Run with input
$ echo "input text" | shortcuts run "My Shortcut"

# View shortcut
$ shortcuts view "My Shortcut"

Quick Look

Preview Files

# Quick Look from command line
$ qlmanage -p file.pdf

# Multiple files
$ qlmanage -p *.png

# Generate thumbnail
$ qlmanage -t -s 256 file.pdf -o /tmp/thumbnails/

# Generate preview
$ qlmanage -p -s 1024 file.pdf -o /tmp/previews/

Quick Look Generator Info

# List Quick Look generators
$ qlmanage -m

# Debug Quick Look for a file
$ qlmanage -d4 -p file.pdf

Services Menu Integration

Create shell-accessible services:

Creating a Service

  1. Open Automator
  2. Create new “Quick Action”
  3. Add “Run Shell Script” action
  4. Write your script
  5. Save with descriptive name

The service appears in:

  • Right-click context menu
  • Application menu → Services

Running Services from Shell

# Services aren't directly callable, but you can:
# 1. Use Automator to create a workflow
# 2. Run the workflow with automator command
$ automator /path/to/service.workflow

Notification Center

Built-in Notification

# Using osascript
$ osascript -e 'display notification "Message" with title "Title" subtitle "Subtitle" sound name "Glass"'

terminal-notifier

More powerful notification tool:

# Install
$ brew install terminal-notifier

# Basic notification
$ terminal-notifier -title "Build" -message "Complete"

# With actions
$ terminal-notifier -title "Alert" -message "Click me" \
    -open "https://apple.com"

# Execute command on click
$ terminal-notifier -title "Alert" -message "Click to run" \
    -execute "echo 'clicked' >> /tmp/log"

# With buttons (macOS 10.9+)
$ terminal-notifier -title "Question" -message "Choose:" \
    -actions "Yes,No" -closeLabel "Maybe"

# Notify when command completes
$ long_command; terminal-notifier -message "Done" -title "Task Complete"

Calendar and Reminders

Calendar Events

# List calendars
$ osascript -e 'tell application "Calendar" to get name of every calendar'

# Create event
$ osascript << 'EOF'
tell application "Calendar"
    tell calendar "Work"
        make new event with properties {
            summary: "Meeting",
            start date: (current date) + 1 * days,
            end date: (current date) + 1 * days + 1 * hours
        }
    end tell
end tell
EOF

Reminders

# Create reminder
$ osascript -e 'tell application "Reminders" to make new reminder with properties {name: "Buy milk"}'

# Create with due date
$ osascript << 'EOF'
tell application "Reminders"
    tell list "Tasks"
        make new reminder with properties {
            name: "Important task",
            due date: (current date) + 2 * days
        }
    end tell
end tell
EOF

Location Services

Core Location

# Get current location (requires permission)
# Create and compile a Swift snippet or use:
$ osascript -e 'tell application "System Events" to get the current location'
# Note: Often restricted

# Alternative: Use whereami tool
$ brew install whereami
$ whereami
Latitude: 37.7749
Longitude: -122.4194

Integration Scripts

Complete Example: Project Opener

Create a script that integrates multiple services:

#!/bin/zsh
# project-open: Open a project with all necessary tools

project_dir="$1"

if [[ ! -d "$project_dir" ]]; then
    terminal-notifier -title "Error" -message "Directory not found"
    exit 1
fi

cd "$project_dir"

# Open in VS Code
open -a "Visual Studio Code" .

# Open Finder
open .

# Open terminal tab
osascript -e "tell application \"Terminal\"
    do script \"cd '$project_dir' && git status\"
end tell"

# Notify
terminal-notifier -title "Project Opened" \
    -message "$(basename $project_dir)" \
    -open "file://$project_dir"

# Play sound
afplay /System/Library/Sounds/Glass.aiff

Clipboard Workflow

#!/bin/zsh
# Process clipboard contents

# Get clipboard
content=$(pbpaste)

# Transform (example: convert to uppercase)
transformed=$(echo "$content" | tr '[:lower:]' '[:upper:]')

# Put back
echo "$transformed" | pbcopy

# Notify
osascript -e 'display notification "Clipboard transformed" with title "Done"'

File Watcher with Actions

#!/bin/zsh
# Watch directory and act on changes

watch_dir="$HOME/Downloads"

fswatch -0 "$watch_dir" | while read -d "" event; do
    filename=$(basename "$event")

    case "$filename" in
        *.pdf)
            # Move PDFs to Documents
            mv "$event" ~/Documents/PDFs/
            terminal-notifier -message "PDF filed: $filename"
            ;;
        *.zip)
            # Extract ZIPs
            unzip -d "${event%.zip}" "$event"
            terminal-notifier -message "Extracted: $filename"
            ;;
    esac
done

Summary

macOS shell integration enables powerful workflows:

ServiceCommand/Method
Finderopen, osascript
Keychainsecurity
AppleScriptosascript
Notificationsosascript, terminal-notifier
Quick Lookqlmanage
Spotlightmdfind, mdls
Shortcutsshortcuts
Calendarosascript
Remindersosascript

Key integration patterns:

  1. Use osascript for GUI application control
  2. Use security for credential management
  3. Use terminal-notifier for user feedback
  4. Combine tools in shell scripts for workflows

The command line on macOS isn’t separate from the graphical system—it’s deeply integrated, enabling automation that bridges both worlds.

Package Management on macOS

Unlike most Unix systems, macOS doesn’t include a native package manager for command-line software. The App Store handles GUI applications, but for developer tools, libraries, and Unix utilities, you need third-party solutions.

This gap has been filled by community projects, with Homebrew emerging as the dominant solution. Understanding how to install, manage, and troubleshoot packages on macOS is essential for any developer or system administrator.

The Package Management Landscape

macOS offers several package management approaches:

Homebrew

The most popular choice. Homebrew provides:

  • 6,000+ formulae (packages)
  • 5,000+ casks (GUI applications)
  • Active community maintenance
  • Simple brew install workflow

MacPorts

An older, more Unix-traditional approach:

  • Builds everything from source by default
  • Extensive package collection
  • More isolation from system
  • Uses /opt/local prefix

Native Installers

Apple’s standard installation methods:

  • .pkg files for command-line tools
  • .dmg disk images for applications
  • Xcode Command Line Tools
  • App Store for sandboxed applications

Language-Specific Managers

Development often requires language-specific tools:

  • pip / pyenv for Python
  • gem / rbenv / rvm for Ruby
  • npm / nvm for Node.js
  • cargo for Rust
  • go get for Go

Why Package Management Matters

Coming from Linux, you might expect:

$ apt install nginx       # Ubuntu
$ dnf install nginx       # Fedora
$ pacman -S nginx         # Arch

On macOS, there’s no equivalent system command. You need Homebrew:

$ brew install nginx

Understanding macOS package management helps you:

  • Install development dependencies reliably
  • Keep software updated
  • Manage conflicts between versions
  • Work across Apple Silicon and Intel Macs
  • Troubleshoot installation issues

What You’ll Learn in This Part

Homebrew: Architecture and Internals explains how Homebrew works—formulae, casks, taps, bottles, and the Cellar—giving you the knowledge to troubleshoot effectively.

Homebrew Best Practices covers daily usage patterns, maintenance routines, and avoiding common pitfalls.

MacPorts: An Alternative Approach presents MacPorts for those who prefer its philosophy or need packages not available in Homebrew.

Native Installers: pkg and dmg covers Apple’s native installation formats and when to use them.

Managing Python Environments addresses the complexity of Python on macOS—system Python, Homebrew Python, pyenv, and virtual environments.

Managing Ruby Environments covers Ruby version management with rbenv or rvm, essential for Rails development.

Managing Node.js Environments explains nvm and other tools for managing multiple Node.js versions.

Comparing Package Managers provides a feature comparison to help you choose the right tool for different situations.

Quick Start: Installing Homebrew

For those eager to get started:

# Install Homebrew
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Follow the post-install instructions to add to PATH
# For Apple Silicon, add to ~/.zprofile:
eval "$(/opt/homebrew/bin/brew shellenv)"

# Verify installation
$ brew doctor

# Install something
$ brew install wget

The following chapters explain what’s happening under the hood and how to use these tools effectively.

Homebrew: Architecture and Internals

Homebrew describes itself as “The Missing Package Manager for macOS.” To use it effectively—especially when things go wrong—you need to understand its architecture: how formulae define packages, how the Cellar stores installations, how taps extend the available packages, and how bottles provide pre-compiled binaries.

Installation Locations

Homebrew’s location depends on your Mac’s architecture:

Apple Silicon (M1/M2/M3)

/opt/homebrew/
├── bin/           → Symlinks to installed binaries
├── Cellar/        → Installed packages (versioned)
├── etc/           → Configuration files
├── include/       → Header files
├── lib/           → Libraries
├── opt/           → Symlinks to latest versions
├── sbin/          → System binaries
├── share/         → Architecture-independent data
└── var/           → Variable data (logs, databases)

Intel Macs

/usr/local/
├── bin/
├── Cellar/
├── etc/
...

Why Different Locations?

Apple Silicon Macs use /opt/homebrew to:

  • Avoid conflicts with Rosetta 2 (Intel emulation)
  • Allow running both native and Intel Homebrew
  • Follow Unix convention (/opt for optional software)

Core Concepts

Formulae

A formula is a Ruby script defining how to install a package:

# View a formula
$ brew cat wget
class Wget < Formula
  desc "Internet file retriever"
  homepage "https://www.gnu.org/software/wget/"
  url "https://ftp.gnu.org/gnu/wget/wget-1.21.4.tar.gz"
  sha256 "81542f5cefb8faacc39bbbc6c82ded..."

  depends_on "openssl@3"
  depends_on "pkg-config" => :build

  def install
    system "./configure", "--prefix=#{prefix}",
                          "--with-ssl=openssl",
                          "--with-openssl-dir=#{Formula["openssl@3"].opt_prefix}"
    system "make", "install"
  end

  test do
    system "#{bin}/wget", "-O", "-", "https://example.com"
  end
end

Key elements:

  • desc: Package description
  • homepage: Project website
  • url: Source code download location
  • sha256: Checksum for verification
  • depends_on: Dependencies
  • install: Build and installation commands
  • test: Verification test

The Cellar

Installed packages live in the Cellar, organized by name and version:

$ ls /opt/homebrew/Cellar/wget/
1.21.4/

$ ls /opt/homebrew/Cellar/wget/1.21.4/
bin/        etc/        lib/        share/

$ ls /opt/homebrew/Cellar/wget/1.21.4/bin/
wget

This versioned structure allows:

  • Multiple versions installed simultaneously
  • Easy rollback (brew switch wget 1.21.3)
  • Clean uninstallation

Homebrew creates symlinks for easy access:

# Symlink in bin
$ ls -la /opt/homebrew/bin/wget
lrwxr-xr-x  1 user  admin  32 Jan 15 10:00 /opt/homebrew/bin/wget -> ../Cellar/wget/1.21.4/bin/wget

# Stable reference in opt
$ ls -la /opt/homebrew/opt/wget
lrwxr-xr-x  1 user  admin  22 Jan 15 10:00 /opt/homebrew/opt/wget -> ../Cellar/wget/1.21.4

The opt directory provides stable paths regardless of version:

  • /opt/homebrew/opt/openssl always points to current OpenSSL
  • Useful for configuring build systems

Bottles

Bottles are pre-compiled binary packages:

# Install from bottle (default)
$ brew install wget
==> Downloading https://ghcr.io/v2/homebrew/core/wget/manifests/1.21.4
==> Pouring wget--1.21.4.arm64_sonoma.bottle.tar.gz

Without bottles, Homebrew compiles from source:

# Force compilation from source
$ brew install --build-from-source wget
==> Downloading https://ftp.gnu.org/gnu/wget/wget-1.21.4.tar.gz
==> ./configure --prefix=/opt/homebrew/Cellar/wget/1.21.4 ...
==> make install

Bottles are:

  • Pre-compiled for specific macOS versions
  • Built for both Intel and Apple Silicon
  • Stored on GitHub Container Registry
  • Much faster than source compilation

Taps

Taps are third-party formula repositories:

# List tapped repositories
$ brew tap
homebrew/core
homebrew/cask
homebrew/services

# Add a tap
$ brew tap hashicorp/tap

# Now you can install from it
$ brew install hashicorp/tap/terraform

# View tap contents
$ brew tap-info hashicorp/tap

Default taps (pre-installed):

  • homebrew/core: Main formula repository
  • homebrew/cask: GUI application installers

Tap mechanics:

# Taps clone Git repositories
$ ls ~/Library/Homebrew/Library/Taps/
homebrew/
hashicorp/

# You can create your own tap
$ brew tap-new myname/mytap

Casks

Casks install macOS GUI applications:

# Install application
$ brew install --cask visual-studio-code

# Casks typically:
# 1. Download .dmg or .zip
# 2. Extract .app bundle
# 3. Move to /Applications

Cask definition example:

cask "visual-studio-code" do
  version "1.85.0"
  sha256 "abc123..."

  url "https://update.code.visualstudio.com/#{version}/darwin-arm64/stable"
  name "Visual Studio Code"
  homepage "https://code.visualstudio.com/"

  app "Visual Studio Code.app"

  zap trash: [
    "~/Library/Application Support/Code",
    "~/.vscode",
  ]
end

Package Lifecycle

Installation

$ brew install python@3.11

# What happens:
# 1. Resolve dependencies
# 2. Download bottle (or source)
# 3. Verify checksum
# 4. Extract to Cellar
# 5. Create symlinks
# 6. Run post-install script

Upgrading

$ brew upgrade python@3.11

# What happens:
# 1. Download new version
# 2. Install to new Cellar directory
# 3. Update symlinks
# 4. Keep old version (until cleanup)

Uninstallation

$ brew uninstall python@3.11

# What happens:
# 1. Remove symlinks
# 2. Remove Cellar directory
# 3. Optional: remove unused dependencies

Dependencies

Viewing Dependencies

# Direct dependencies
$ brew deps wget
gettext
libidn2
openssl@3

# Full dependency tree
$ brew deps --tree wget
wget
├── gettext
├── libidn2
│   ├── gettext
│   └── libunistring
└── openssl@3
    └── ca-certificates

# Why is something installed?
$ brew uses --installed openssl@3
curl
python@3.11
wget

Dependency Types

  • Required: Must be installed
  • Build: Only needed during compilation
  • Optional: Enables additional features
  • Recommended: Suggested but not required
# In formula
depends_on "cmake" => :build          # Build-time only
depends_on "openssl@3"                # Runtime required
depends_on "readline" => :optional    # Optional feature

Services

Homebrew can manage background services:

# Start a service
$ brew services start postgresql@14

# Stop a service
$ brew services stop postgresql@14

# Restart
$ brew services restart postgresql@14

# List services
$ brew services list
Name          Status  User File
postgresql@14 started user ~/Library/LaunchAgents/homebrew.mxcl.postgresql@14.plist

# Service files are launchd plists
$ cat ~/Library/LaunchAgents/homebrew.mxcl.postgresql@14.plist

Important Files and Directories

# Homebrew itself
/opt/homebrew/                          # Prefix (Apple Silicon)
/opt/homebrew/bin/brew                  # Main executable
/opt/homebrew/Homebrew/                 # Homebrew Git repository

# Installed packages
/opt/homebrew/Cellar/                   # All installations
/opt/homebrew/opt/                      # Stable symlinks

# Configuration
/opt/homebrew/etc/                      # Config files

# Cache
~/Library/Caches/Homebrew/              # Downloaded files
~/Library/Caches/Homebrew/downloads/    # Bottles and sources

# Logs
~/Library/Logs/Homebrew/                # Build logs

# Taps
$(brew --repository)/Library/Taps/      # Tap repositories

Under the Hood

Git-Based Design

Homebrew itself is a Git repository:

# View Homebrew version
$ brew --version
Homebrew 4.2.0

# Update Homebrew (git pull)
$ brew update
==> Updating Homebrew...

# View homebrew commits
$ cd $(brew --repository) && git log --oneline -5

Formula repositories are also Git-based:

# View formula history
$ brew log wget

Keg-Only Packages

Some packages are “keg-only”—installed but not symlinked:

$ brew info openssl@3
==> openssl@3: stable 3.2.0 (bottled)
...
openssl@3 is keg-only, which means it was not symlinked into /opt/homebrew,
because macOS provides LibreSSL.

# Access keg-only packages
$ /opt/homebrew/opt/openssl@3/bin/openssl version

# Or add to PATH
export PATH="/opt/homebrew/opt/openssl@3/bin:$PATH"

Keg-only reasons:

  • Conflicts with macOS system software
  • Multiple versions available
  • Not meant for direct use

Linking and Unlinking

# Unlink (remove symlinks but keep installed)
$ brew unlink python@3.11

# Link (create symlinks)
$ brew link python@3.11

# Force link keg-only formula (use carefully)
$ brew link --force openssl@3

Diagnostic Commands

# Check Homebrew health
$ brew doctor
Your system is ready to brew.

# Show configuration
$ brew config
HOMEBREW_VERSION: 4.2.0
ORIGIN: https://github.com/Homebrew/brew
HEAD: abc1234
Core tap ORIGIN: https://github.com/Homebrew/homebrew-core
...

# Debug information
$ brew --env
HOMEBREW_CC: clang
HOMEBREW_CXX: clang++
...

# List all installed packages
$ brew list

# List with versions
$ brew list --versions

# Show what would be upgraded
$ brew outdated

Summary

Homebrew architecture key concepts:

ComponentPurposeLocation
FormulaPackage definitionTaps (Git repos)
CellarInstalled packages/opt/homebrew/Cellar/
BottlesPre-built binariesGitHub Container Registry
TapsFormula repositories~/.../Taps/
CasksGUI app installershomebrew/cask tap
optStable symlinks/opt/homebrew/opt/

Understanding this architecture helps you:

  • Troubleshoot installation issues
  • Manage multiple versions
  • Create your own formulae
  • Work with keg-only packages
  • Understand why things are where they are

Homebrew Best Practices

Homebrew is straightforward to use but benefits from disciplined practices. This chapter covers daily usage patterns, maintenance routines, managing upgrades, and avoiding common pitfalls.

Daily Usage

Installing Packages

# Search for packages
$ brew search postgresql
==> Formulae
postgresql@11    postgresql@14    postgresql@15    postgresql@16

# Get information before installing
$ brew info postgresql@16
==> postgresql@16: stable 16.1 (bottled)
Object-relational database system
...

# Install
$ brew install postgresql@16

# Install specific version
$ brew install postgresql@14

# Install from source (if needed)
$ brew install --build-from-source package

Installing GUI Applications

# Search casks
$ brew search --cask firefox

# Install cask
$ brew install --cask firefox

# Casks go to /Applications by default
# Verify installation
$ ls /Applications/Firefox.app

Updating and Upgrading

# Update Homebrew itself and formula definitions
$ brew update

# See what's outdated
$ brew outdated

# Upgrade all packages
$ brew upgrade

# Upgrade specific package
$ brew upgrade wget

# Upgrade casks
$ brew upgrade --cask

# Pin a package (prevent upgrades)
$ brew pin postgresql@16
$ brew list --pinned
$ brew unpin postgresql@16

Uninstalling

# Uninstall a package
$ brew uninstall wget

# Uninstall with dependencies check
$ brew uninstall --zap firefox  # For casks: removes preferences too

# Remove unused dependencies
$ brew autoremove

Maintenance Routine

Regular Maintenance Commands

Run these periodically (weekly or monthly):

# Update everything
$ brew update && brew upgrade

# Clean up old versions
$ brew cleanup

# Check for problems
$ brew doctor

# Remove unused dependencies
$ brew autoremove

Cleanup Details

# Preview what cleanup would remove
$ brew cleanup --dry-run

# Clean up older than default (120 days)
$ brew cleanup --prune=30  # 30 days

# Clean specific package
$ brew cleanup wget

# See disk usage before/after
$ du -sh $(brew --cache)
1.2G    /Users/david/Library/Caches/Homebrew
$ brew cleanup
$ du -sh $(brew --cache)
256M    /Users/david/Library/Caches/Homebrew

Dealing with Doctor Warnings

$ brew doctor
Please note that these warnings are just used to help the Homebrew maintainers
with debugging if you file an issue. If everything you use Homebrew for is
working fine: please don't worry or file an issue; just ignore this. Thanks!

Warning: Some installed formulae are deprecated or disabled.
...

Common warnings and fixes:

Unbrewed files in Homebrew directories:

Warning: Unbrewed dylibs were found in /opt/homebrew/lib
# Usually safe to ignore, or remove manually if you know they're orphaned

Outdated Xcode Command Line Tools:

Warning: A newer Command Line Tools release is available.
$ softwareupdate --list
$ softwareupdate -i "Command Line Tools for Xcode-15.0"
# Or just update Xcode from App Store

Broken symlinks:

Warning: Broken symlinks were found
$ brew doctor --list-checks | xargs brew
# Or manually remove listed broken symlinks

Managing Multiple Versions

Version-Specific Packages

# Install specific versions
$ brew install python@3.11
$ brew install python@3.12

# Both are installed but one is linked
$ brew list | grep python
python@3.11
python@3.12

# Switch versions
$ brew unlink python@3.11
$ brew link python@3.12

# Check which is active
$ python3 --version

Using Unlinked Versions

Access unlinked versions directly:

# Direct path
$ /opt/homebrew/opt/python@3.11/bin/python3 --version
Python 3.11.7

# Or add to PATH for session
$ export PATH="/opt/homebrew/opt/python@3.11/bin:$PATH"

Dealing with Dependencies

Understanding Dependency Problems

# See why a package is installed
$ brew uses --installed openssl@3
curl
git
python@3.11
wget

# See what a package needs
$ brew deps wget
gettext
libidn2
openssl@3

# Full dependency tree
$ brew deps --tree --installed

Dependency Conflicts

When upgrades break dependencies:

# Reinstall package and dependencies
$ brew reinstall wget

# Rebuild all dependents of a package
$ brew reinstall $(brew uses --installed openssl@3)

Services Management

Using brew services

# List all services
$ brew services list

# Start service (runs at login)
$ brew services start postgresql@16

# Run once (doesn't persist across reboot)
$ brew services run postgresql@16

# Stop service
$ brew services stop postgresql@16

# Restart service
$ brew services restart postgresql@16

# View service status
$ brew services info postgresql@16

Service Files

# Location of service plists
$ ls ~/Library/LaunchAgents/homebrew.*
homebrew.mxcl.postgresql@16.plist

# View service configuration
$ cat ~/Library/LaunchAgents/homebrew.mxcl.postgresql@16.plist

# Manual service management
$ launchctl list | grep homebrew

Bundler: Reproducible Environments

Creating a Brewfile

# Generate Brewfile from current installations
$ brew bundle dump

# Creates Brewfile:
$ cat Brewfile
tap "homebrew/bundle"
tap "homebrew/cask"
tap "homebrew/core"
brew "git"
brew "node"
brew "python@3.11"
cask "visual-studio-code"
cask "docker"

Installing from Brewfile

# Install everything in Brewfile
$ brew bundle

# Check what would be installed
$ brew bundle check

# List what's in Brewfile but not installed
$ brew bundle list

Brewfile Best Practices

# Brewfile with options and documentation

# Taps
tap "homebrew/bundle"
tap "homebrew/cask-fonts"

# Development tools
brew "git"
brew "gh"  # GitHub CLI
brew "neovim"

# Languages
brew "python@3.11"
brew "node"
brew "go"

# Databases
brew "postgresql@16", restart_service: true
brew "redis"

# Applications
cask "visual-studio-code"
cask "docker"
cask "iterm2"

# Fonts
cask "font-fira-code"

# Mac App Store apps (requires mas)
# mas "Xcode", id: 497799835

Troubleshooting

Common Issues

“Error: No available formula with the name…”

$ brew install nonexistent
Error: No available formula with the name "nonexistent"

# Solution: Update and search
$ brew update
$ brew search partial-name

“Error: Cannot install in Homebrew on ARM…”

# This happens when mixing architectures
# Ensure you're using the right Homebrew
$ which brew
/opt/homebrew/bin/brew  # Should be this on Apple Silicon

Permission errors:

$ brew install something
Error: Permission denied @ rb_sysopen

# Fix permissions
$ sudo chown -R $(whoami) $(brew --prefix)/*

Bottle not available:

# Install from source when bottle unavailable
$ brew install --build-from-source package

Resetting Homebrew

When things are really broken:

# Nuclear option: Uninstall Homebrew
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"

# Then reinstall
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Less nuclear:

# Reset to clean state
$ brew update-reset

# Fix all symlinks
$ brew link --overwrite --dry-run $(brew list --formula)
$ brew link --overwrite $(brew list --formula)

Security Considerations

Verifying Packages

# Homebrew verifies checksums automatically
# View checksum
$ brew info --json wget | jq '.[0].versions.bottle_sha256'

# Audit a formula
$ brew audit wget

Avoiding Untrusted Taps

# List your taps
$ brew tap

# Only use trusted taps
# Official: homebrew/core, homebrew/cask
# Verify third-party taps before adding

Cask Security

# Some casks require passwords - be cautious
# Check what a cask does before installing
$ brew cat --cask suspicious-app

# Review artifacts and scripts

Performance Tips

Faster Updates

# Use parallel downloads
$ export HOMEBREW_PARALLEL_FETCH=4

# Skip analytics
$ brew analytics off

Reducing Disk Usage

# Regular cleanup
$ brew cleanup --prune=all

# Check what's using space
$ brew list --formula | xargs -I{} sh -c 'echo -n "{}: " && du -sh $(brew --cellar)/{} | cut -f1'

# Remove old versions aggressively
$ brew cleanup --prune=0

Cache Management

# View cache
$ ls ~/Library/Caches/Homebrew/

# Clear cache
$ rm -rf ~/Library/Caches/Homebrew/*

# Or selectively
$ brew cleanup --prune=all

CI/CD Usage

GitHub Actions Example

- name: Install Homebrew dependencies
  run: |
    brew update
    brew install cmake ninja

# Or with Brewfile
- name: Install from Brewfile
  run: brew bundle

Caching in CI

- name: Cache Homebrew
  uses: actions/cache@v3
  with:
    path: |
      ~/Library/Caches/Homebrew
      /opt/homebrew/Cellar
    key: ${{ runner.os }}-brew-${{ hashFiles('Brewfile.lock.json') }}

Summary

Best practices checklist:

PracticeCommand
Update regularlybrew update && brew upgrade
Clean upbrew cleanup
Check healthbrew doctor
Remove orphansbrew autoremove
Pin critical packagesbrew pin package
Use Brewfilebrew bundle dump
Audit before adding tapsResearch tap source

Golden rules:

  1. Update before installingbrew update first
  2. Use bottles when possible — Much faster than source
  3. Run cleanup regularly — Saves disk space
  4. Use Brewfile for teams — Reproducible environments
  5. Don’t sudo brew — Never run Homebrew with sudo
  6. Check doctor warnings — Not all need fixing, but awareness helps

MacPorts: An Alternative Approach

MacPorts predates Homebrew and takes a different philosophical approach to package management on macOS. While Homebrew focuses on ease of use and leveraging macOS system libraries, MacPorts prioritizes isolation, reproducibility, and Unix tradition. Understanding MacPorts helps you make informed choices about which tool to use—and some software is only available in MacPorts.

MacPorts Philosophy

MacPorts follows principles from the BSD ports tradition:

  1. Self-contained: Installs entirely under /opt/local, separate from system
  2. Build from source: Compiles everything by default
  3. Explicit variants: Fine-grained control over build options
  4. Full dependency trees: Doesn’t rely on macOS system libraries

This approach offers predictability and control at the cost of longer installation times.

Installation

Prerequisites

MacPorts requires Xcode or Command Line Tools:

$ xcode-select --install

Installing MacPorts

Download the installer from macports.org for your macOS version, or install manually:

# Download (check macports.org for current version)
$ curl -O https://distfiles.macports.org/MacPorts/MacPorts-2.8.1.tar.bz2
$ tar xf MacPorts-2.8.1.tar.bz2
$ cd MacPorts-2.8.1

# Build and install
$ ./configure
$ make
$ sudo make install

Post-Installation

Add to your PATH in ~/.zprofile:

export PATH="/opt/local/bin:/opt/local/sbin:$PATH"
export MANPATH="/opt/local/share/man:$MANPATH"

Verify:

$ source ~/.zprofile
$ port version
Version: 2.8.1

Basic Usage

Searching for Ports

# Search by name
$ port search postgresql
postgresql15 @15.4 (databases)
    PostgreSQL is a highly-extensible, object-relational database...

# Search descriptions too
$ port search --description "web server"

# List all ports
$ port list

Getting Information

$ port info nginx
nginx @1.24.0 (www)
Variants:             debug, gzip_static, http2, mail, realip...

Description:          Nginx is a high performance HTTP server
Homepage:             https://nginx.org/

Build Dependencies:   pcre
Library Dependencies: openssl, pcre, zlib
Platforms:            darwin
License:              BSD
Maintainers:          Email: ...

Installing Ports

# Install (requires sudo)
$ sudo port install nginx

# Install with variants
$ sudo port install nginx +http2 +realip

# Install specific version
$ sudo port install postgresql15 @15.4

# Dry run (see what would happen)
$ port -y install nginx

Updating

# Update port definitions
$ sudo port selfupdate

# Upgrade all outdated ports
$ sudo port upgrade outdated

# Upgrade specific port
$ sudo port upgrade nginx

# See what's outdated
$ port outdated

Uninstalling

# Uninstall a port
$ sudo port uninstall nginx

# Uninstall with dependents
$ sudo port uninstall --follow-dependents nginx

# Remove inactive ports (old versions)
$ sudo port uninstall inactive

Variants

Variants are build options that customize port installation:

# List variants for a port
$ port variants vim
vim has the variants:
   athena: Use Athena widgets for GUI
   big: Build big features
   cscope: Enable cscope interface
   gtk2: Enable GTK2 GUI
   gtk3: Enable GTK3 GUI
   huge: Build huge features
   ...

# Install with variant
$ sudo port install vim +huge +python311

# Install without default variant
$ sudo port install vim -x11

# View installed variants
$ port installed vim
  vim @9.0.1000_0+huge+python311 (active)

Default Variants

Configure default variants in /opt/local/etc/macports/variants.conf:

# Always build with these variants
+no_x11
+quartz

# Never build with these
-debug

Port Lifecycle

Version Management

# List installed versions
$ port installed
  nginx @1.24.0_0 (active)
  nginx @1.22.0_0

# Activate specific version
$ sudo port activate nginx @1.22.0_0

# Deactivate (without uninstalling)
$ sudo port deactivate nginx

Cleaning

# Clean build artifacts for a port
$ sudo port clean nginx

# Clean all ports
$ sudo port clean --all all

# Clean work directories, distribution files, and archives
$ sudo port clean --all --dist nginx

File Locations

/opt/local/
├── bin/              # Binaries
├── etc/              # Configuration files
├── include/          # Header files
├── lib/              # Libraries
├── libexec/          # Support programs
├── sbin/             # System binaries
├── share/            # Architecture-independent files
├── var/              # Variable data (logs, databases)
│   └── macports/     # MacPorts-specific
│       ├── build/    # Build directories
│       └── sources/  # Downloaded sources
└── Library/
    └── Frameworks/   # macOS frameworks

Maintenance

Regular Maintenance

# Full update cycle
$ sudo port selfupdate
$ sudo port upgrade outdated
$ sudo port uninstall inactive
$ sudo port clean --all all

Reclaiming Disk Space

# Remove inactive ports
$ sudo port uninstall inactive

# Remove unrequested ports (installed as dependencies but no longer needed)
$ sudo port uninstall leaves

# Clean all working directories and sources
$ sudo port clean --all installed
$ sudo rm -rf /opt/local/var/macports/distfiles/*

Checking Installation

# Verify installed ports
$ port -v installed

# Check for broken ports
$ port diagnose

# List dependencies
$ port deps nginx
$ port rdeps nginx  # Recursive
$ port dependents nginx  # What depends on this

MacPorts vs Homebrew

Key Differences

AspectMacPortsHomebrew
Installation prefix/opt/local/opt/homebrew or /usr/local
Default buildFrom sourcePre-built bottles
System integrationSelf-containedUses macOS libraries
Sudo requiredYesNo
Build customizationVariantsLimited options
Package count~27,000~6,000 formulae
SpeedSlower (compiles)Faster (bottles)

When to Use MacPorts

  • Need specific build options: Variants offer fine-grained control
  • Isolation is important: Complete separation from system
  • Package only in MacPorts: Some software not in Homebrew
  • Reproducible builds: Same configuration everywhere
  • Server environments: Predictable, isolated installations

When to Use Homebrew

  • Quick installation: Bottles are fast
  • macOS integration: Works with system libraries
  • Casual use: Simpler command syntax
  • GUI apps: Casks are convenient
  • Most common packages: Available and well-maintained

Coexistence

You can run both MacPorts and Homebrew:

# Different paths, different prefixes
/opt/local/bin/python3          # MacPorts
/opt/homebrew/bin/python3        # Homebrew

# Adjust PATH to prefer one
export PATH="/opt/homebrew/bin:$PATH"       # Prefer Homebrew
# or
export PATH="/opt/local/bin:$PATH"          # Prefer MacPorts

Caveats:

  • Don’t mix libraries from both in builds
  • Be explicit about which version you’re using
  • Can cause confusion with shared dependencies

Creating Custom Ports

Portfile Basics

# /opt/local/var/macports/sources/.../myport/Portfile

PortSystem          1.0

name                myapp
version             1.0.0
categories          devel
platforms           darwin
maintainers         {example.com:admin}
description         My application
long_description    ${description} with more details

homepage            https://example.com/myapp
master_sites        https://example.com/downloads/

checksums           rmd160  abc123... \
                    sha256  def456...

depends_lib         port:openssl

configure.args      --with-ssl=${prefix}

post-destroot {
    xinstall -d ${destroot}${prefix}/share/doc/${name}
    xinstall -m 644 ${worksrcpath}/README ${destroot}${prefix}/share/doc/${name}
}

Local Portfile Repository

# Create local repository
$ mkdir -p ~/ports/mycat/myport
$ cp Portfile ~/ports/mycat/myport/

# Add to sources.conf
$ sudo nano /opt/local/etc/macports/sources.conf
# Add line:
file:///Users/username/ports

# Update index
$ cd ~/ports
$ portindex

# Now install
$ sudo port install myport

Troubleshooting

Common Issues

Port fails to build:

# View build log
$ port logfile nginx
$ less /opt/local/var/macports/logs/_opt_local_var_macports_sources_.../nginx/main.log

# Try clean build
$ sudo port clean nginx
$ sudo port install nginx

Dependency conflicts:

# See dependency graph
$ port deps nginx

# Force rebuild of dependencies
$ sudo port -n upgrade --force nginx

Stuck port:

# Force uninstall
$ sudo port -f uninstall nginx

# Clean state
$ sudo port clean nginx
$ sudo rm -rf /opt/local/var/macports/build/*nginx*

Recovering from Problems

# Selfupdate when things are broken
$ sudo port -d selfupdate

# Restore ports from backup
$ sudo port -f restore /path/to/backup.tar.gz

# Migration after macOS upgrade
$ sudo port migrate

Summary

MacPorts provides:

  • Self-contained installation in /opt/local
  • Build-from-source with variants for customization
  • Large package collection
  • Traditional Unix approach

Best for users who:

  • Need specific build configurations
  • Prefer isolation from system
  • Have time for source compilation
  • Need packages not in Homebrew

Commands reference:

ActionCommand
Searchport search name
Infoport info name
Installsudo port install name
Updatesudo port selfupdate
Upgradesudo port upgrade outdated
Uninstallsudo port uninstall name
Cleansudo port uninstall inactive

Native Installers: pkg and dmg

macOS has its own installation formats that predate third-party package managers. Understanding .pkg installers and .dmg disk images helps you manage software that doesn’t come through Homebrew or MacPorts, including Xcode Command Line Tools and many commercial applications.

Package Types Overview

FormatDescriptionManagement
.pkgInstaller packagepkgutil, installer
.dmgDisk image containing .apphdiutil, manual
.appApplication bundleManual drag to /Applications
.mpkgMeta-package (multiple .pkg)installer

.pkg Installers

What pkg Files Contain

A .pkg file is an archive containing:

  • Payload (files to install)
  • Scripts (pre/post install)
  • Bill of Materials (file manifest)
  • Package info
# Examine pkg contents
$ pkgutil --payload-files /path/to/package.pkg
$ pkgutil --bom /path/to/package.pkg

Installing pkg Files

# GUI installation
$ open package.pkg

# Command-line installation (requires sudo)
$ sudo installer -pkg package.pkg -target /
installer: Package name is My Package
installer: Installing at base path /
installer: The install was successful.

# Install to specific volume
$ sudo installer -pkg package.pkg -target /Volumes/OtherDisk

# Verbose installation
$ sudo installer -pkg package.pkg -target / -verbose

Managing Installed Packages

# List all installed packages
$ pkgutil --pkgs
com.apple.pkg.CLTools_Executables
com.apple.pkg.CLTools_SDK_macOS14
...

# Filter by pattern
$ pkgutil --pkgs | grep -i xcode

# Package information
$ pkgutil --pkg-info com.apple.pkg.CLTools_Executables
package-id: com.apple.pkg.CLTools_Executables
version: 15.0.0.0.1.1694021235
volume: /
location: /
install-time: 1694000000

# Files installed by a package
$ pkgutil --files com.apple.pkg.CLTools_Executables | head
Library/Developer/CommandLineTools/
Library/Developer/CommandLineTools/SDKs/
Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
...

# Find which package owns a file
$ pkgutil --file-info /usr/bin/git
volume: /
path: /usr/bin/git
pkgid: com.apple.pkg.CLTools_Executables
...

Removing Packages

macOS doesn’t have a built-in package uninstaller. Removal requires:

# 1. Find installed files
$ pkgutil --files com.example.package > /tmp/files.txt

# 2. Review the file list
$ cat /tmp/files.txt

# 3. Remove files (carefully!)
$ cd /
$ sudo rm -rf $(pkgutil --files com.example.package)

# 4. Forget the package receipt
$ sudo pkgutil --forget com.example.package

Warning: Be very careful with rm -rf. Always review file lists first.

Xcode Command Line Tools

The most common .pkg on developer Macs:

# Check if installed
$ xcode-select -p
/Library/Developer/CommandLineTools

# Install
$ xcode-select --install
# Opens GUI prompt

# Or via softwareupdate
$ softwareupdate --list | grep "Command Line Tools"
$ softwareupdate -i "Command Line Tools for Xcode-15.0"

# Switch between Xcode and CLI tools
$ sudo xcode-select --switch /Applications/Xcode.app
$ sudo xcode-select --switch /Library/Developer/CommandLineTools

# Reset
$ sudo xcode-select --reset

Creating pkg Files

For distributing your own software:

# Create package from directory
$ pkgbuild --root /path/to/files \
           --identifier com.example.myapp \
           --version 1.0.0 \
           --install-location /Applications \
           myapp.pkg

# With scripts
$ pkgbuild --root /path/to/files \
           --identifier com.example.myapp \
           --version 1.0.0 \
           --scripts /path/to/scripts \
           --install-location /Applications \
           myapp.pkg

# Create distribution (combines multiple packages)
$ productbuild --distribution distribution.xml \
               --package-path /path/to/packages \
               final-installer.pkg

.dmg Disk Images

Working with DMG Files

# Mount disk image
$ hdiutil attach application.dmg
/dev/disk4          GUID_partition_scheme
/dev/disk4s1        Apple_APFS
/dev/disk5s1        Apple_APFS                   /Volumes/Application

# List mounted images
$ hdiutil info | grep image-path
image-path      : /path/to/application.dmg

# Unmount
$ hdiutil detach /Volumes/Application
# or
$ hdiutil detach /dev/disk5

Installing from DMG

Typical DMG workflow:

# Mount
$ hdiutil attach MyApp.dmg

# Copy app to Applications
$ cp -R /Volumes/MyApp/MyApp.app /Applications/

# Unmount
$ hdiutil detach /Volumes/MyApp

# Optional: delete dmg
$ rm MyApp.dmg

DMG Scripted Installation

#!/bin/bash
# install-from-dmg.sh

DMG_PATH="$1"
APP_NAME="$2"

# Mount
MOUNT_POINT=$(hdiutil attach "$DMG_PATH" -nobrowse | grep "/Volumes" | tail -1 | cut -f3)

# Copy
cp -R "$MOUNT_POINT"/*.app /Applications/ 2>/dev/null || \
cp -R "$MOUNT_POINT/$APP_NAME" /Applications/

# Unmount
hdiutil detach "$MOUNT_POINT"

echo "Installed to /Applications"

Creating DMG Files

# Create DMG from folder
$ hdiutil create -srcfolder /path/to/MyApp.app \
                 -volname "MyApp" \
                 -format UDZO \
                 MyApp.dmg

# Create empty DMG
$ hdiutil create -size 100m \
                 -fs HFS+ \
                 -volname "MyApp" \
                 MyApp.dmg

# Create read-write DMG (for customization)
$ hdiutil create -srcfolder /path/to/MyApp.app \
                 -volname "MyApp" \
                 -format UDRW \
                 MyApp-rw.dmg

# Convert to compressed read-only
$ hdiutil convert MyApp-rw.dmg -format UDZO -o MyApp.dmg

Internet-Enabled DMG

DMGs can auto-extract:

# Make DMG internet-enabled
$ hdiutil internet-enable -yes MyApp.dmg

When downloaded via Safari, the app is extracted and DMG deleted automatically.

Application Bundles (.app)

Bundle Structure

$ ls -la /Applications/Safari.app/Contents/
total 24
drwxr-xr-x   7 root  wheel   224 Dec 11 11:23 .
drwxr-xr-x   3 root  wheel    96 Dec 11 11:23 ..
drwxr-xr-x   3 root  wheel    96 Dec 11 11:23 _CodeSignature
-rw-r--r--   1 root  wheel  8572 Dec 11 11:23 Info.plist
drwxr-xr-x   3 root  wheel    96 Dec 11 11:23 MacOS
-rw-r--r--   1 root  wheel     8 Dec 11 11:23 PkgInfo
drwxr-xr-x  73 root  wheel  2336 Dec 11 11:23 Resources
drwxr-xr-x   5 root  wheel   160 Dec 11 11:23 XPCServices

# Key files
/Contents/Info.plist         # Application metadata
/Contents/MacOS/             # Executable
/Contents/Resources/         # App resources
/Contents/_CodeSignature/    # Code signature

App Metadata

# View Info.plist
$ plutil -p /Applications/Safari.app/Contents/Info.plist | head -20
{
  "BuildMachineOSBuild" => "23A344"
  "CFBundleDevelopmentRegion" => "English"
  "CFBundleExecutable" => "Safari"
  "CFBundleIdentifier" => "com.apple.Safari"
  "CFBundleInfoDictionaryVersion" => "6.0"
  "CFBundleName" => "Safari"
  "CFBundlePackageType" => "APPL"
  ...
}

# Get specific values
$ defaults read /Applications/Safari.app/Contents/Info.plist CFBundleVersion
17617.1.17.11.9

Installing Applications

# Copy to Applications
$ cp -R /path/to/MyApp.app /Applications/

# Or per-user
$ cp -R /path/to/MyApp.app ~/Applications/

# Remove quarantine (for trusted apps)
$ xattr -d com.apple.quarantine /Applications/MyApp.app

Uninstalling Applications

# Remove app bundle
$ rm -rf /Applications/MyApp.app

# Also remove:
# - ~/Library/Application Support/MyApp
# - ~/Library/Preferences/com.example.myapp.plist
# - ~/Library/Caches/com.example.myapp

# Find related files
$ mdfind "kMDItemCFBundleIdentifier == 'com.example.myapp'"
$ mdfind -name "myapp"

Software Update

Command-Line Software Updates

# List available updates
$ softwareupdate --list
Software Update found the following new or updated software:
* Label: macOS Sonoma 14.2.1-23C71
...

# Install all updates
$ sudo softwareupdate --install --all

# Install specific update
$ sudo softwareupdate --install "macOS Sonoma 14.2.1-23C71"

# Download only (don't install)
$ sudo softwareupdate --download --all

# Install and restart if needed
$ sudo softwareupdate --install --all --restart

Managing Update Behavior

# Check automatic update settings
$ defaults read /Library/Preferences/com.apple.SoftwareUpdate

# Enable automatic updates
$ sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool true
$ sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -bool true

# Disable automatic updates
$ sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool false

App Store from Command Line

mas (Mac App Store CLI)

# Install mas
$ brew install mas

# Sign in (may require App Store sign-in first)
$ mas signin email@example.com

# Search
$ mas search Xcode
497799835  Xcode (15.0)

# Install by ID
$ mas install 497799835

# List installed
$ mas list

# Outdated apps
$ mas outdated

# Upgrade all
$ mas upgrade

Summary

Native macOS installation methods:

MethodUse CaseManagement
.pkgSystem tools, CLTpkgutil, installer
.dmgApp distributionhdiutil
.appDirect app bundlecp, rm
App StoreSandboxed appsmas
softwareupdatemacOS/systemsoftwareupdate

Key commands:

TaskCommand
Install pkgsudo installer -pkg file.pkg -target /
List packagespkgutil --pkgs
Package filespkgutil --files com.example.pkg
Forget packagesudo pkgutil --forget com.example.pkg
Mount dmghdiutil attach file.dmg
Unmount dmghdiutil detach /Volumes/Name
System updatessoftwareupdate --list
Install CLTxcode-select --install

Native installers complement package managers for software that requires system-level installation or comes directly from vendors.

Managing Python Environments

Python on macOS is notoriously confusing. There’s the system Python (don’t use it), Homebrew Python (convenient), pyenv (version management), and the official python.org installers. Add virtual environments and you have a recipe for confusion. This chapter untangles the mess.

Python Versions on macOS

System Python

macOS historically included Python, but:

  • macOS 12.3+: No Python pre-installed (finally!)
  • Older macOS: Python 2.7 at /usr/bin/python (deprecated)
  • Never modify system Python: It’s for macOS internals
# Check if system Python exists
$ /usr/bin/python --version
zsh: /usr/bin/python: bad interpreter: No such file or directory

# Modern macOS has no /usr/bin/python
# But may have /usr/bin/python3 from Xcode CLT
$ /usr/bin/python3 --version
Python 3.9.6

Which Python Should You Use?

SourceRecommendation
System PythonNever — don’t modify
Xcode CLT PythonFor basic scripting only
HomebrewGood for single-version needs
pyenvRecommended — version management
python.orgAlternative to pyenv

Homebrew Python

Installation

# Install latest Python
$ brew install python

# Or specific version
$ brew install python@3.11
$ brew install python@3.12

# Check installation
$ brew info python

Location and Linking

# Where is it?
$ which python3
/opt/homebrew/bin/python3

$ ls -la /opt/homebrew/bin/python3*
lrwxr-xr-x  1 user  admin  42 Jan 15 10:00 /opt/homebrew/bin/python3 -> ../Cellar/python@3.12/3.12.0/bin/python3
lrwxr-xr-x  1 user  admin  49 Jan 15 10:00 /opt/homebrew/bin/python3.12 -> ../Cellar/python@3.12/3.12.0/bin/python3.12

# Actual location
$ /opt/homebrew/opt/python@3.12/bin/python3 --version

Managing Multiple Homebrew Pythons

# Install multiple versions
$ brew install python@3.11 python@3.12

# By default, one is linked
$ python3 --version
Python 3.12.0

# Use specific version directly
$ python3.11 --version
$ python3.12 --version

# Or switch linked version
$ brew unlink python@3.12
$ brew link python@3.11

Homebrew pip

# pip is version-specific
$ pip3 --version
pip 23.3.1 from /opt/homebrew/lib/python3.12/site-packages/pip (python 3.12)

# Install packages
$ pip3 install requests

# Where packages go
$ python3 -c "import site; print(site.getsitepackages())"
['/opt/homebrew/lib/python3.12/site-packages']

pyenv: Version Management

pyenv lets you install and switch between Python versions easily.

Installing pyenv

# Install with Homebrew
$ brew install pyenv pyenv-virtualenv

# Or from source
$ curl https://pyenv.run | bash

Shell Configuration

Add to ~/.zprofile and ~/.zshrc:

# ~/.zprofile
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"

# ~/.zshrc
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

Restart your shell or source ~/.zshrc.

Using pyenv

# List available versions
$ pyenv install --list | grep "^  3\."
  3.10.13
  3.11.7
  3.12.0
  ...

# Install a version
$ pyenv install 3.11.7
Downloading Python-3.11.7.tar.xz...
Installing Python-3.11.7...
Installed Python-3.11.7 to /Users/david/.pyenv/versions/3.11.7

# List installed versions
$ pyenv versions
  system
  3.11.7
  3.12.0
* 3.12.0 (set by /Users/david/.pyenv/version)

# Set global default
$ pyenv global 3.11.7

# Set local version (per directory)
$ cd myproject
$ pyenv local 3.11.7
# Creates .python-version file

# Set for current shell only
$ pyenv shell 3.10.13

pyenv Build Dependencies

pyenv compiles Python from source. Install dependencies first:

$ brew install openssl readline sqlite3 xz zlib tcl-tk

# Set build flags (in ~/.zshrc)
export LDFLAGS="-L$(brew --prefix openssl@3)/lib -L$(brew --prefix readline)/lib -L$(brew --prefix sqlite3)/lib -L$(brew --prefix zlib)/lib"
export CPPFLAGS="-I$(brew --prefix openssl@3)/include -I$(brew --prefix readline)/include -I$(brew --prefix sqlite3)/include -I$(brew --prefix zlib)/include"

Virtual Environments

Virtual environments isolate project dependencies.

Built-in venv

# Create virtual environment
$ python3 -m venv myenv

# Activate
$ source myenv/bin/activate
(myenv) $

# Now pip installs to this env
(myenv) $ pip install requests
(myenv) $ which python
/path/to/myenv/bin/python

# Deactivate
(myenv) $ deactivate
$

pyenv-virtualenv

Combined version and environment management:

# Create virtualenv with specific Python
$ pyenv virtualenv 3.11.7 myproject-env

# List virtualenvs
$ pyenv virtualenvs

# Activate
$ pyenv activate myproject-env
(myproject-env) $

# Or set local (auto-activates in directory)
$ pyenv local myproject-env

# Deactivate
$ pyenv deactivate

# Delete virtualenv
$ pyenv virtualenv-delete myproject-env

Poetry

Modern dependency management:

# Install Poetry
$ curl -sSL https://install.python-poetry.org | python3 -
# Or
$ brew install poetry

# New project
$ poetry new myproject
$ cd myproject

# Add dependencies
$ poetry add requests

# Install from existing pyproject.toml
$ poetry install

# Run in environment
$ poetry run python script.py

# Activate shell
$ poetry shell

pipenv

Alternative to Poetry:

# Install
$ brew install pipenv

# Create environment and install packages
$ pipenv install requests

# Activate
$ pipenv shell

# Run command
$ pipenv run python script.py

For Most Developers

  1. Install pyenv:
$ brew install pyenv pyenv-virtualenv
# Add shell configuration
  1. Install Python versions:
$ pyenv install 3.11.7
$ pyenv install 3.12.0
$ pyenv global 3.11.7
  1. Use virtual environments per project:
$ cd myproject
$ pyenv virtualenv 3.11.7 myproject
$ pyenv local myproject

For Data Science

Consider Conda/Miniconda for scientific packages:

# Install Miniconda
$ brew install --cask miniconda

# Initialize
$ conda init zsh

# Create environment
$ conda create -n datascience python=3.11 numpy pandas scikit-learn

# Activate
$ conda activate datascience

Troubleshooting

“python: command not found”

# Check what's available
$ which python3
$ pyenv which python

# Create alias if needed (in ~/.zshrc)
alias python=python3
alias pip=pip3

Wrong Python Version

# Check which Python
$ which python3
$ python3 --version

# Check pyenv
$ pyenv version
$ pyenv which python

# Common issue: Homebrew Python overriding pyenv
# Ensure pyenv is early in PATH
$ echo $PATH | tr ':' '\n' | head

pip Install Permission Errors

# Never use sudo pip!
# Use virtual environment instead
$ python3 -m venv myenv
$ source myenv/bin/activate
$ pip install package

SSL Certificate Errors

# Ensure certificates are installed
$ /Applications/Python\ 3.x/Install\ Certificates.command
# Or
$ pip install certifi
$ python -c "import certifi; print(certifi.where())"

pyenv install Fails

# Install build dependencies
$ brew install openssl readline sqlite3 xz zlib

# Set compiler flags
$ CFLAGS="-I$(brew --prefix openssl)/include -I$(brew --prefix readline)/include" \
  LDFLAGS="-L$(brew --prefix openssl)/lib -L$(brew --prefix readline)/lib" \
  pyenv install 3.11.7

Summary

Python management strategy:

ScenarioRecommendation
Single Python versionHomebrew python@3.x
Multiple versionspyenv
Project isolationvenv or pyenv-virtualenv
Dependency managementPoetry or pip + requirements.txt
Data scienceConda/Miniconda

Key commands:

TaskCommand
Install Python (Homebrew)brew install python@3.11
Install Python (pyenv)pyenv install 3.11.7
Set global versionpyenv global 3.11.7
Set project versionpyenv local 3.11.7
Create virtualenvpython3 -m venv env
Activate virtualenvsource env/bin/activate
Install packagespip install package

Golden rules:

  1. Never modify system Python
  2. Always use virtual environments
  3. Pick one version manager (pyenv recommended)
  4. Document Python version in projects (.python-version or pyproject.toml)

Managing Ruby Environments

macOS includes a system Ruby, but like system Python, you shouldn’t rely on it for development. Ruby version managers solve the problem of needing different Ruby versions for different projects. This chapter covers rbenv (recommended), rvm, and chruby—plus managing gems effectively.

System Ruby

macOS includes Ruby, but it’s outdated and restricted:

$ /usr/bin/ruby --version
ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin23]

# System Ruby is:
# - Old (2.6.x)
# - Requires sudo for gem install
# - Modified by macOS updates
# - Not suitable for development

Rule: Never use system Ruby for development.

rbenv is lightweight and follows Unix philosophy—it does one thing well.

Installation

# Install rbenv and ruby-build
$ brew install rbenv ruby-build

# Verify installation
$ rbenv --version
rbenv 1.2.0

Shell Configuration

Add to ~/.zshrc:

# Initialize rbenv
eval "$(rbenv init - zsh)"

Restart shell or source ~/.zshrc.

Installing Ruby Versions

# List available versions
$ rbenv install -l
3.2.2
3.3.0
jruby-9.4.5.0
truffleruby-23.1.2
...

# Install a version
$ rbenv install 3.2.2
Downloading ruby-3.2.2.tar.gz...
Installing ruby-3.2.2...
Installed ruby-3.2.2 to /Users/david/.rbenv/versions/3.2.2

# List installed versions
$ rbenv versions
  system
* 3.2.2 (set by /Users/david/.rbenv/version)

Switching Versions

# Set global default
$ rbenv global 3.2.2

# Set local version (per directory)
$ cd myproject
$ rbenv local 3.1.4
# Creates .ruby-version file

# Set for current shell
$ rbenv shell 3.0.6

# Check current version
$ rbenv version
3.2.2 (set by /Users/david/.rbenv/version)

$ ruby --version
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin23]

How rbenv Works

rbenv uses shims—wrapper scripts that intercept Ruby commands:

# Shims directory
$ ls ~/.rbenv/shims
bundle  erb  gem  irb  rake  rdoc  ri  ruby

# Shim intercepts and redirects
$ which ruby
/Users/david/.rbenv/shims/ruby

# Actual Ruby
$ rbenv which ruby
/Users/david/.rbenv/versions/3.2.2/bin/ruby

Rehashing

After installing gems with executables, rehash:

$ rbenv rehash
# Creates shims for new executables

# Modern rbenv auto-rehashes, but manual rehash if needed

chruby: Minimal Alternative

chruby is even simpler than rbenv—no shims, just environment modification.

Installation

$ brew install chruby ruby-install

Configuration

Add to ~/.zshrc:

source /opt/homebrew/opt/chruby/share/chruby/chruby.sh
source /opt/homebrew/opt/chruby/share/chruby/auto.sh  # Auto-switching

Usage

# Install Ruby
$ ruby-install ruby 3.2.2

# List Rubies
$ chruby
   ruby-3.2.2

# Switch
$ chruby ruby-3.2.2

# Auto-switching reads .ruby-version
$ echo "ruby-3.2.2" > .ruby-version
$ cd .  # Triggers auto-switch

RVM (Ruby Version Manager) is feature-rich but more complex.

Installation

# Install rvm
$ \curl -sSL https://get.rvm.io | bash -s stable

# Restart shell or source
$ source ~/.rvm/scripts/rvm

Usage

# Install Ruby
$ rvm install 3.2.2

# List versions
$ rvm list

# Switch versions
$ rvm use 3.2.2

# Set default
$ rvm --default use 3.2.2

# Create gemset (isolated gem environment)
$ rvm gemset create myproject
$ rvm use 3.2.2@myproject

# Project-specific .rvmrc
$ echo "rvm use 3.2.2@myproject" > .rvmrc

rvm vs rbenv

Featurerbenvrvm
PhilosophyMinimalFull-featured
GemsetsNo (use bundler)Yes
Shell modificationShimsPATH modification
ComplexityLowHigher
RecommendationPreferredIf you need gemsets

Gem Management

Understanding Gems

# Gem environment
$ gem env
RubyGems Environment:
  - RUBYGEMS VERSION: 3.4.10
  - RUBY VERSION: 3.2.2
  - INSTALLATION DIRECTORY: /Users/david/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0
  - USER INSTALLATION DIRECTORY: /Users/david/.gem/ruby/3.2.0
  - GEM PATHS:
     - /Users/david/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0
     - /Users/david/.gem/ruby/3.2.0

# Install gem
$ gem install rails

# List installed gems
$ gem list

# Uninstall
$ gem uninstall rails

Bundler: Project Dependencies

Bundler manages gems per-project:

# Install bundler
$ gem install bundler

# Create Gemfile
$ bundle init

# Edit Gemfile
$ cat Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 7.0'
gem 'puma'

# Install dependencies
$ bundle install

# Run command with bundled gems
$ bundle exec rails server

# Update gems
$ bundle update

# Lock to specific versions
$ bundle lock

Gemfile Best Practices

# Gemfile
source 'https://rubygems.org'

# Specify Ruby version
ruby '3.2.2'

# Specify gem versions
gem 'rails', '~> 7.0.8'
gem 'pg', '~> 1.5'

# Development-only gems
group :development do
  gem 'rubocop'
  gem 'pry'
end

group :test do
  gem 'rspec'
end

Gem Configuration

# ~/.gemrc
gem: --no-document

# Skip documentation for faster installs
$ gem install rails --no-document

Rails Development Setup

Complete setup for Rails development:

# 1. Install rbenv
$ brew install rbenv ruby-build

# 2. Configure shell
$ echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc
$ source ~/.zshrc

# 3. Install Ruby
$ rbenv install 3.2.2
$ rbenv global 3.2.2

# 4. Install Bundler
$ gem install bundler

# 5. Install Rails
$ gem install rails

# 6. Verify
$ rails --version
Rails 7.1.2

# 7. Create project
$ rails new myapp
$ cd myapp
$ bundle install
$ bin/rails server

Troubleshooting

“gem install” Permission Denied

# Don't use sudo!
# Ensure you're using rbenv Ruby, not system
$ which ruby
/Users/david/.rbenv/shims/ruby  # Good

$ which ruby
/usr/bin/ruby  # Bad - system Ruby

# Fix: Check rbenv setup
$ rbenv versions
$ rbenv global 3.2.2

Command Not Found After gem install

# Rehash to create shims
$ rbenv rehash

# Or check if gem's bin is in PATH
$ gem env | grep "EXECUTABLE DIRECTORY"

Build Failures

# Install build dependencies
$ brew install openssl readline libyaml

# Set build flags
$ RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(brew --prefix openssl@3)" rbenv install 3.2.2

SSL Certificate Errors

# Update certificates
$ brew install openssl
$ rbenv install 3.2.2
# openssl from Homebrew is used automatically

# Or update system certificates
$ security find-certificate -a -p /Library/Keychains/System.keychain > certs.pem

Wrong Ruby Version in Project

# Check .ruby-version
$ cat .ruby-version
3.1.4

# Install if missing
$ rbenv install 3.1.4

# cd out and back in
$ cd .. && cd myproject
$ ruby --version

Summary

Ruby version management:

ToolBest For
rbenvMost users (recommended)
chrubyMinimalists
rvmUsers needing gemsets
Homebrew rubySingle version, casual use

Key commands (rbenv):

TaskCommand
Install rbenvbrew install rbenv ruby-build
Install Rubyrbenv install 3.2.2
Set globalrbenv global 3.2.2
Set localrbenv local 3.2.2
List versionsrbenv versions
Install gemgem install name
Project gemsbundle install

Best practices:

  1. Never use system Ruby
  2. Use rbenv for version management
  3. Use Bundler for project dependencies
  4. Commit Gemfile.lock to version control
  5. Specify Ruby version in .ruby-version

Managing Node.js Environments

Node.js development often requires different versions for different projects—an older LTS for production, the latest for new features, or specific versions matching your CI environment. Version managers like nvm, fnm, and volta solve this problem, while package managers like npm, yarn, and pnpm handle dependencies.

Node.js Installation Options

MethodProsCons
nvmMost popular, well-supportedShell startup overhead
fnmFast, Rust-basedNewer, less documentation
voltaAlso manages npm/yarnNewer tool
HomebrewSimpleSingle version only
Official installerDirect from sourceManual version management

nvm: Node Version Manager

nvm is the most widely used Node.js version manager.

Installation

# Install nvm
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# Or with Homebrew (note: Homebrew version has caveats)
$ brew install nvm

Shell Configuration

Add to ~/.zshrc:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # Load nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # Completion

Restart shell or source ~/.zshrc.

Using nvm

# List available versions
$ nvm ls-remote
        v18.18.0   (LTS: Hydrogen)
        v18.19.0   (Latest LTS: Hydrogen)
        v20.10.0   (LTS: Iron)
        v21.5.0
...

# Install a version
$ nvm install 20  # Installs latest v20.x
$ nvm install 18.19.0  # Specific version
$ nvm install --lts  # Latest LTS

# List installed versions
$ nvm ls
->     v20.10.0
       v18.19.0
default -> 20 (-> v20.10.0)
...

# Switch versions
$ nvm use 18
Now using node v18.19.0 (npm v10.2.3)

# Set default
$ nvm alias default 20

# Use in current directory (.nvmrc)
$ echo "20" > .nvmrc
$ nvm use
Found '/path/to/project/.nvmrc' with version <20>
Now using node v20.10.0 (npm v10.2.3)

Auto-switching

Automatically switch when entering directories:

# Add to ~/.zshrc
autoload -U add-zsh-hook
load-nvmrc() {
  local nvmrc_path="$(nvm_find_nvmrc)"
  if [ -n "$nvmrc_path" ]; then
    local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")
    if [ "$nvmrc_node_version" = "N/A" ]; then
      nvm install
    elif [ "$nvmrc_node_version" != "$(nvm version)" ]; then
      nvm use
    fi
  elif [ -n "$(PWD=$OLDPWD nvm_find_nvmrc)" ] && [ "$(nvm version)" != "$(nvm version default)" ]; then
    echo "Reverting to nvm default version"
    nvm use default
  fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc

nvm Performance

nvm can slow shell startup. Mitigate with lazy loading:

# Lazy load nvm (in ~/.zshrc)
export NVM_DIR="$HOME/.nvm"

nvm() {
  unset -f nvm node npm npx
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
  nvm "$@"
}

node() {
  unset -f nvm node npm npx
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
  node "$@"
}

npm() {
  unset -f nvm node npm npx
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
  npm "$@"
}

npx() {
  unset -f nvm node npm npx
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
  npx "$@"
}

fnm: Fast Node Manager

fnm is a Rust-based alternative that’s significantly faster.

Installation

# Via Homebrew
$ brew install fnm

# Or curl
$ curl -fsSL https://fnm.vercel.app/install | bash

Configuration

# Add to ~/.zshrc
eval "$(fnm env --use-on-cd)"

Usage

# Install Node
$ fnm install 20
$ fnm install --lts

# List installed
$ fnm list
* v20.10.0 default
  v18.19.0

# Use version
$ fnm use 18

# Set default
$ fnm default 20

# Auto-switching via .nvmrc or .node-version
$ fnm use  # Reads from .nvmrc

fnm vs nvm

Featurefnmnvm
SpeedFast (Rust)Slower (shell)
.nvmrc supportYesYes
CompletionsYesYes
Windows supportYesNo (use nvm-windows)
MaturityNewerEstablished

volta: Toolchain Manager

Volta manages Node.js plus npm/yarn versions per project.

Installation

$ curl https://get.volta.sh | bash

Usage

# Install Node
$ volta install node@20
$ volta install node@lts

# Install global tools
$ volta install yarn
$ volta install typescript

# Pin version in project
$ volta pin node@18
# Updates package.json with volta field

# Now anyone with volta uses same versions

How volta Differs

Volta pins tool versions in package.json:

{
  "name": "myproject",
  "volta": {
    "node": "18.19.0",
    "npm": "10.2.3"
  }
}

This ensures consistent tooling across team members.

Package Managers

npm (default)

npm comes with Node.js:

# Check version
$ npm --version

# Install dependencies
$ npm install

# Add package
$ npm install lodash

# Add dev dependency
$ npm install --save-dev jest

# Global install
$ npm install -g typescript

# Update packages
$ npm update

# Audit security
$ npm audit
$ npm audit fix

yarn

Yarn offers performance improvements and workspaces:

# Install yarn
$ npm install -g yarn
# Or with corepack (Node 16.10+)
$ corepack enable
$ corepack prepare yarn@stable --activate

# Install dependencies
$ yarn install

# Add package
$ yarn add lodash
$ yarn add --dev jest

# Update
$ yarn upgrade

# Workspaces (monorepo)
$ yarn workspaces list

pnpm

pnpm saves disk space via hard links:

# Install pnpm
$ npm install -g pnpm
# Or with corepack
$ corepack enable
$ corepack prepare pnpm@latest --activate

# Install dependencies
$ pnpm install

# Add package
$ pnpm add lodash
$ pnpm add -D jest

# Disk savings
$ pnpm store path
$ pnpm store prune

Choosing a Package Manager

ManagerBest For
npmDefault, wide compatibility
yarnWorkspaces, performance
pnpmDisk efficiency, strict

Global Packages

Where They Go

# npm global location
$ npm root -g
/Users/david/.nvm/versions/node/v20.10.0/lib/node_modules

# yarn global location
$ yarn global dir
/Users/david/.config/yarn/global

Managing Globals

# List globals
$ npm list -g --depth=0
$ yarn global list

# Install global
$ npm install -g typescript
$ yarn global add typescript

# Update globals
$ npm update -g
$ yarn global upgrade

Globals with Version Managers

Global packages are per Node version:

# Install in Node 20
$ nvm use 20
$ npm install -g typescript

# Switch to Node 18 - typescript not available
$ nvm use 18
$ which tsc
tsc not found

# Need to install again
$ npm install -g typescript

For shared globals, use npx instead:

# Run without installing
$ npx typescript --version
$ npx create-react-app myapp

Project Configuration

.nvmrc / .node-version

# Create version file
$ echo "20" > .nvmrc
# Or specific
$ echo "20.10.0" > .nvmrc

# Works with nvm, fnm, and others
$ nvm use

package.json engines

{
  "engines": {
    "node": ">=18.0.0",
    "npm": ">=9.0.0"
  }
}

Enforced with npm install --engine-strict.

Corepack

Node 16.10+ includes Corepack for package manager management:

# Enable corepack
$ corepack enable

# Specify in package.json
{
  "packageManager": "yarn@4.0.2"
}

# Now yarn is auto-installed at correct version
$ yarn --version
4.0.2

Troubleshooting

“node: command not found” After nvm install

# Check nvm is loaded
$ command -v nvm

# Reload shell config
$ source ~/.zshrc

# Set default
$ nvm alias default 20
$ nvm use default

Permission Errors

# Never use sudo with nvm-installed node
# If you have permission issues, nvm isn't set up right
$ which node
/Users/david/.nvm/versions/node/v20.10.0/bin/node  # Good
/usr/local/bin/node  # Bad - not nvm

Wrong Node in Scripts

# Scripts may use different PATH
# Add to script or use full path
#!/usr/bin/env node

# Or ensure PATH includes nvm
export PATH="$NVM_DIR/versions/node/$(nvm version)/bin:$PATH"

node-gyp Build Errors

# Install build tools
$ xcode-select --install

# For Python dependency
$ brew install python

Summary

Node.js management strategy:

ToolRecommendation
Version managerfnm (fast) or nvm (established)
Package managernpm (default), yarn/pnpm for specific needs
Project config.nvmrc + engines in package.json

Key commands (nvm):

TaskCommand
Install nvmcurl -o- https://raw.githubusercontent.com/.../nvm/.../install.sh | bash
Install Nodenvm install 20
Use versionnvm use 20
Set defaultnvm alias default 20
Project versionecho "20" > .nvmrc
List versionsnvm ls

Best practices:

  1. Use a version manager (nvm or fnm)
  2. Include .nvmrc in projects
  3. Set engines in package.json
  4. Don’t sudo npm install -g
  5. Use npx for one-off commands

Comparing Package Managers

With multiple package management options on macOS, choosing the right tool matters. This chapter provides a comprehensive comparison to help you make informed decisions for different scenarios.

General Package Managers

Homebrew vs MacPorts

AspectHomebrewMacPorts
PhilosophySimple, macOS-integratedTraditional Unix, self-contained
Installation prefix/opt/homebrew (AS) or /usr/local (Intel)/opt/local
Sudo requiredNoYes
Default installationPre-built bottlesBuild from source
Build timeFast (bottles)Slow (compilation)
Build customizationLimited optionsVariants system
Package count~6,000 formulae + ~5,000 casks~27,000 ports
GUI appsCasksSome available
System integrationUses macOS librariesSelf-contained
UpdatesRollingRolling
CommunityVery activeActive
DocumentationExcellentGood

When to Choose Homebrew

  • Quick installations: Bottles install in seconds
  • macOS GUI apps: Casks make it easy
  • Developer workflows: Most tutorials assume Homebrew
  • Casual use: Simple commands, minimal configuration
  • Standard packages: Common tools well-supported

When to Choose MacPorts

  • Build customization: Variants offer fine-grained control
  • Isolation: Complete separation from system
  • Reproducibility: Same build everywhere
  • Obscure packages: Larger package collection
  • Server environments: Predictable configurations

Language Version Managers

Python: pyenv vs Others

ToolMechanismVirtual EnvsComplexity
pyenvShimsVia pyenv-virtualenvMedium
HomebrewCellarVia venvLow
CondaEnvironmentBuilt-inHigh
asdfShimsVia pluginMedium

Recommendation: pyenv for most Python development. Conda for data science.

Ruby: rbenv vs rvm vs chruby

Featurerbenvrvmchruby
MechanismShimsPATH modificationPATH modification
GemsetsNo (use Bundler)YesNo
ComplexityLowHighVery low
Shell startupFastSlowerFastest
FeaturesMinimalFull-featuredMinimal
DocumentationGoodExtensiveBasic

Recommendation: rbenv for most users. chruby for minimalists. rvm if you need gemsets.

Node.js: nvm vs fnm vs volta

Featurenvmfnmvolta
ImplementationShell scriptRustRust
SpeedSlow startupFastFast
WindowsNoYesYes
.nvmrc supportYesYesNo (uses package.json)
Package manager pinningNoNoYes
MaturityVery matureMatureNewer

Recommendation: fnm for speed. nvm for compatibility. volta for teams needing consistent tooling.

Universal Version Managers

asdf: One Tool for Everything

asdf manages multiple languages with plugins:

# Install asdf
$ brew install asdf

# Add plugins
$ asdf plugin add nodejs
$ asdf plugin add python
$ asdf plugin add ruby

# Install versions
$ asdf install nodejs 20.10.0
$ asdf install python 3.11.7
$ asdf install ruby 3.2.2

# Set versions
$ asdf global nodejs 20.10.0
$ asdf local python 3.11.7

# .tool-versions file
nodejs 20.10.0
python 3.11.7
ruby 3.2.2

Pros:

  • Single tool for all languages
  • Consistent interface
  • One configuration file

Cons:

  • Plugin quality varies
  • Another abstraction layer
  • May lag behind native managers

mise (formerly rtx)

Modern asdf alternative in Rust:

$ brew install mise

# Compatible with asdf plugins and .tool-versions
$ mise install node@20
$ mise use node@20

Pros: Faster than asdf, compatible with asdf plugins.

Decision Framework

For Individual Developers

Need GUI apps?
├─ Yes → Homebrew (casks)
└─ No → Either works

Need build customization?
├─ Yes → MacPorts
└─ No → Homebrew

Multiple Python versions?
├─ Yes → pyenv
└─ No → Homebrew Python

Multiple Ruby versions?
├─ Yes → rbenv
└─ No → Homebrew Ruby

Multiple Node versions?
├─ Yes → fnm or nvm
└─ No → Homebrew Node

Many languages?
├─ Yes → Consider asdf or mise
└─ No → Use language-specific managers

For Teams

  • Consistency: Use Brewfile and version files
  • Documentation: Document required tools in README
  • CI/CD: Match local and CI environments
# Brewfile for team
tap "homebrew/bundle"
brew "git"
brew "node"
brew "python@3.11"

# .tool-versions for asdf users
nodejs 20.10.0
python 3.11.7
ruby 3.2.2

# Or individual version files
# .nvmrc, .ruby-version, .python-version

For Servers/Production

ScenarioRecommendation
DockerOS package manager in container
Bare metalMacPorts (isolation) or native packages
CI runnersMatch developer environment

Package Manager Combinations

Common setups that work well together:

Minimal Setup

# Just Homebrew
brew install python node ruby
# Single versions, simple

Standard Developer Setup

# Homebrew for system tools
brew install git wget jq

# Language version managers
brew install pyenv rbenv nvm

# Configure each
pyenv install 3.11.7
rbenv install 3.2.2
nvm install 20
# Homebrew for tools and casks
brew install git wget
brew install --cask visual-studio-code docker

# asdf for all languages
brew install asdf
asdf plugin add python nodejs ruby golang

# Or individual managers if preferred

Avoiding Conflicts

PATH Priority

# Check what's first in PATH
$ echo $PATH | tr ':' '\n' | head -5

# Typical priority order:
# 1. Version manager shims (~/.pyenv/shims)
# 2. Homebrew (/opt/homebrew/bin)
# 3. Local (/usr/local/bin)
# 4. System (/usr/bin)

Detecting Conflicts

# Which python am I using?
$ which python3
$ python3 --version

# Are there multiple?
$ type -a python3
python3 is /Users/david/.pyenv/shims/python3
python3 is /opt/homebrew/bin/python3
python3 is /usr/bin/python3

Resolving Conflicts

# Remove Homebrew version if using version manager
$ brew uninstall python

# Or unlink
$ brew unlink python

# Ensure version manager is in PATH first
export PATH="$HOME/.pyenv/shims:$PATH"

Summary

Quick Reference

NeedSolution
System toolsHomebrew
GUI appsHomebrew Casks
Multiple Python versionspyenv
Multiple Ruby versionsrbenv
Multiple Node versionsfnm
All languagesasdf or mise
Build customizationMacPorts
IsolationMacPorts
Enterprise/serverMacPorts or native

Best Practices

  1. Don’t mix Homebrew and MacPorts for the same package
  2. Use version managers for languages you develop in
  3. Document your setup in project READMEs
  4. Use Brewfile for reproducible tool installation
  5. Keep things simple - more tools ≠ better
  6. Match CI environment to local development

For most macOS developers:

# Package manager
Homebrew

# Languages (pick what you need)
pyenv + pyenv-virtualenv  # Python
rbenv                      # Ruby
fnm                        # Node.js

# Or if using many languages
asdf or mise

# Project configuration
.python-version, .ruby-version, .nvmrc
Brewfile for team tools

This combination provides flexibility, isolation, and reproducibility without excessive complexity.

System Services on macOS

If you’re coming from Linux, you’ll search for systemctl and find nothing. macOS uses launchd, a fundamentally different approach to service management that Apple introduced in 2005. Understanding launchd is essential for managing background processes, scheduling tasks, and running services on macOS.

What Is launchd?

launchd is macOS’s unified service management framework. It replaces:

  • init (process 1)
  • cron (scheduled tasks)
  • inetd (network services)
  • xinetd (extended inetd)
  • rc.d scripts (startup scripts)

launchd is always PID 1:

$ ps aux | head -2
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root     1   0.1  0.1 410148544  18624   ??  Ss   10:00AM   0:05.23 /sbin/launchd

Key Concepts

Jobs vs Services

In launchd terminology:

  • Job: Any task managed by launchd (one-time or recurring)
  • Daemon: A background process running as root
  • Agent: A background process running as a user

Property Lists (plists)

launchd jobs are configured via property list files:

# System-wide daemons
/Library/LaunchDaemons/

# System-wide agents
/Library/LaunchAgents/

# Per-user agents
~/Library/LaunchAgents/

# Apple's daemons (don't modify)
/System/Library/LaunchDaemons/

# Apple's agents (don't modify)
/System/Library/LaunchAgents/

launchctl

launchctl is the command-line interface to launchd:

# List all services
$ launchctl list

# Load/start a service
$ launchctl load /Library/LaunchDaemons/com.example.daemon.plist

# Unload/stop a service
$ launchctl unload /Library/LaunchDaemons/com.example.daemon.plist

What You’ll Learn in This Part

launchd: The Heart of macOS provides a comprehensive overview of launchd architecture, how it boots the system, and its role in process management.

Understanding Property Lists (plists) explains the XML and binary formats used for launchd configuration and how to create and edit them.

Creating Launch Agents and Daemons walks through creating your own background services step by step.

launchctl Command Reference is a comprehensive guide to the launchctl command and all its subcommands.

Process Management: CLI vs GUI covers managing processes from both Terminal and Activity Monitor.

Background Task Scheduling explains how to schedule recurring tasks using launchd (the macOS way) and cron (the traditional Unix way).

XPC Services and IPC introduces Apple’s modern inter-process communication framework.

Comparing launchd to systemd and init helps Linux users understand how macOS’s approach differs from what they know.

Quick Example

Here’s a simple launch agent that runs a script every hour:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.hourly</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/myscript.sh</string>
    </array>
    <key>StartInterval</key>
    <integer>3600</integer>
</dict>
</plist>

Save as ~/Library/LaunchAgents/com.example.hourly.plist and load:

$ launchctl load ~/Library/LaunchAgents/com.example.hourly.plist

The following chapters explain this and much more in detail.

launchd: The Heart of macOS

launchd is the first process started by the macOS kernel and the ancestor of all other processes. Understanding launchd is fundamental to understanding how macOS manages services, schedules tasks, and controls the lifecycle of processes.

launchd’s Role

launchd serves multiple functions that are separate on other Unix systems:

FunctionTraditional UnixmacOS
Init systeminit, systemdlaunchd
Service managersystemd, rc.dlaunchd
Task schedulercronlaunchd
Socket activationinetd, xinetdlaunchd
Session managementPAM, loginlaunchd

The Boot Process

When macOS boots:

  1. Firmware (iBoot on Apple Silicon) loads the kernel
  2. Kernel starts and launches /sbin/launchd as PID 1
  3. launchd reads system configuration from:
    • /System/Library/LaunchDaemons/ (Apple services)
    • /Library/LaunchDaemons/ (third-party system services)
  4. launchd starts essential system services
  5. loginwindow appears (GUI login)
  6. User session launches per-user launchd
  7. Per-user launchd loads:
    • /System/Library/LaunchAgents/ (Apple user agents)
    • /Library/LaunchAgents/ (system-wide user agents)
    • ~/Library/LaunchAgents/ (user agents)
Kernel
    │
    └── launchd (PID 1, root)
            │
            ├── System daemons
            │   ├── mds (Spotlight)
            │   ├── configd (network)
            │   ├── bluetoothd
            │   └── ...
            │
            ├── loginwindow
            │   │
            │   └── User session launchd (per user)
            │           │
            │           ├── User agents
            │           │   ├── Dock
            │           │   ├── Finder
            │           │   └── ...
            │           │
            │           └── User applications
            │
            └── Background daemons

Daemons vs Agents

Daemons

  • Run as root (or specified user)
  • Start at boot, before user login
  • System-wide scope
  • Located in LaunchDaemons directories
  • Can’t access GUI or user session
# System daemons location
$ ls /Library/LaunchDaemons/
com.apple.installer.osmessagetracing.plist
homebrew.mxcl.postgresql@14.plist
...

Agents

  • Run as the logged-in user
  • Start at user login
  • Per-user scope
  • Located in LaunchAgents directories
  • Can access GUI, user session
# User agents location
$ ls ~/Library/LaunchAgents/
com.example.myagent.plist
...

Which to Use?

NeedUse
Run before user loginDaemon
Access user sessionAgent
Run as rootDaemon
Run as current userAgent
System-wide serviceDaemon
Per-user serviceAgent

Job States

launchd jobs have several states:

# View job status
$ launchctl list | grep com.example
-       0       com.example.myjob
# Columns: PID, Exit Status, Label

# PID "-" means not running
# Exit Status "0" means last run succeeded

States:

  • Loaded: Job definition is registered
  • Running: Process is executing
  • Waiting: Waiting for trigger (time, socket, path)
  • Stopped: Not running, can be started

On-Demand Loading

launchd’s key innovation is on-demand loading. Services can be:

Always Running (KeepAlive)

<key>KeepAlive</key>
<true/>

launchd restarts the job if it exits.

Socket Activated

<key>Sockets</key>
<dict>
    <key>Listeners</key>
    <dict>
        <key>SockServiceName</key>
        <string>http</string>
    </dict>
</dict>

launchd listens on the socket; starts service when connection arrives.

Time-Based

<key>StartInterval</key>
<integer>3600</integer>

Runs every 3600 seconds.

Path-Based

<key>WatchPaths</key>
<array>
    <string>/path/to/watch</string>
</array>

Runs when file/directory changes.

launchd Domains

Modern launchctl organizes jobs into domains:

DomainDescriptionExample
systemSystem-wide servicessystem/com.apple.mds
userPer-user servicesuser/501/com.example.agent
guiGUI session servicesgui/501/com.apple.Dock
loginLogin sessionlogin/501
pidPer-processpid/12345
# List jobs in user domain
$ launchctl print user/$(id -u)

# List jobs in GUI domain
$ launchctl print gui/$(id -u)

Working with launchd

Basic Commands

# List all loaded jobs
$ launchctl list

# Load a job
$ launchctl load /path/to/job.plist

# Unload a job
$ launchctl unload /path/to/job.plist

# Start a loaded job
$ launchctl start com.example.myjob

# Stop a running job
$ launchctl stop com.example.myjob

Modern Commands (macOS 10.10+)

# Bootstrap (load and enable)
$ launchctl bootstrap gui/$(id -u) /path/to/job.plist

# Bootout (unload)
$ launchctl bootout gui/$(id -u)/com.example.myjob

# Enable job to run at load
$ launchctl enable user/$(id -u)/com.example.myjob

# Disable job
$ launchctl disable user/$(id -u)/com.example.myjob

# Kick (force immediate run)
$ launchctl kickstart gui/$(id -u)/com.example.myjob

# Kill and restart
$ launchctl kickstart -k gui/$(id -u)/com.example.myjob

Debugging

# Print detailed job info
$ launchctl print gui/$(id -u)/com.example.myjob

# Print domain info
$ launchctl print gui/$(id -u)

# Check why job isn't loading
$ launchctl print-disabled user/$(id -u)

# View launchd logs
$ log show --predicate 'subsystem == "com.apple.launchd"' --last 1h

Common launchd Keys

KeyPurpose
LabelUnique identifier (required)
ProgramArgumentsCommand to run
ProgramSimple program path
RunAtLoadStart when loaded
KeepAliveRestart if exits
StartIntervalRun every N seconds
StartCalendarIntervalRun at specific times
WatchPathsRun when paths change
QueueDirectoriesRun when directories have files
StandardOutPathRedirect stdout
StandardErrorPathRedirect stderr
WorkingDirectorySet working directory
EnvironmentVariablesSet environment
UserNameRun as user (daemons)
GroupNameRun as group (daemons)

Log Output

By default, launchd job output goes to the unified log. To capture output:

<key>StandardOutPath</key>
<string>/tmp/myjob.stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/myjob.stderr.log</string>

Or view in Console.app / unified log:

$ log show --predicate 'process == "myjob"' --last 1h

Security Considerations

Disabled Jobs

macOS can disable jobs for security:

# Check disabled status
$ launchctl print-disabled user/$(id -u)
"com.example.suspicious" => disabled

Code Signing

System integrity checks may prevent unsigned jobs from loading.

Sandbox

launchd jobs can be sandboxed:

<key>EnableSandboxing</key>
<true/>

Summary

launchd key concepts:

ConceptDescription
PID 1First process, parent of all
DaemonSystem-wide background service
AgentPer-user background service
On-demandServices start when needed
DomainOrganizational scope
plistXML/binary configuration
launchctlCommand-line interface

launchd is more than an init system—it’s a comprehensive process management framework that enables macOS’s responsive behavior by starting services only when needed.

Understanding Property Lists (plists)

Property lists are macOS’s standard format for structured data. launchd configurations, application preferences, and system settings all use plists. Understanding how to read, create, and modify plists is essential for macOS system administration.

Plist Formats

Property lists can be stored in multiple formats:

FormatExtensionHuman ReadablePerformance
XML.plistYesSlower
Binary.plistNoFaster
JSON.jsonYesMedium
# Check plist format
$ file /Library/LaunchDaemons/com.apple.example.plist
/Library/LaunchDaemons/com.apple.example.plist: XML 1.0 document text, ASCII text

$ file /System/Library/LaunchDaemons/com.apple.mds.plist
/System/Library/LaunchDaemons/com.apple.mds.plist: Apple binary property list

XML Plist Structure

Basic Example

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.myjob</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/myscript</string>
        <string>--flag</string>
        <string>argument</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StartInterval</key>
    <integer>300</integer>
</dict>
</plist>

Data Types

TypeXML ElementExample
String<string><string>hello</string>
Integer<integer><integer>42</integer>
Real<real><real>3.14</real>
Boolean<true/> or <false/><true/>
Date<date><date>2024-01-15T10:30:00Z</date>
Data (binary)<data><data>SGVsbG8=</data>
Array<array>See below
Dictionary<dict>See below

Arrays

<key>MyArray</key>
<array>
    <string>item1</string>
    <string>item2</string>
    <integer>42</integer>
</array>

Nested Dictionaries

<key>ParentDict</key>
<dict>
    <key>ChildKey</key>
    <string>value</string>
    <key>NestedDict</key>
    <dict>
        <key>DeepKey</key>
        <string>deep value</string>
    </dict>
</dict>

Command-Line Tools

plutil

Validate and convert plists:

# Validate syntax
$ plutil /path/to/file.plist
/path/to/file.plist: OK

# Convert to XML
$ plutil -convert xml1 binary.plist

# Convert to binary
$ plutil -convert binary1 file.plist

# Convert to JSON
$ plutil -convert json file.plist -o file.json

# Pretty print (plutil -p)
$ plutil -p file.plist
{
  "Label" => "com.example.myjob"
  "ProgramArguments" => [
    0 => "/usr/local/bin/myscript"
    1 => "--flag"
  ]
  "RunAtLoad" => 1
}

defaults

Read and write preference plists:

# Read all values
$ defaults read com.apple.finder

# Read specific key
$ defaults read com.apple.finder ShowExternalHardDrivesOnDesktop
1

# Write value
$ defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool false

# Delete key
$ defaults delete com.apple.finder ShowExternalHardDrivesOnDesktop

# Read arbitrary plist file
$ defaults read /path/to/file.plist

# Type specifiers: -string, -int, -float, -bool, -data, -array, -dict
$ defaults write com.example.app count -int 42
$ defaults write com.example.app enabled -bool true

PlistBuddy

More powerful plist editing:

# Read value
$ /usr/libexec/PlistBuddy -c "Print :Label" file.plist
com.example.myjob

# Write value
$ /usr/libexec/PlistBuddy -c "Set :StartInterval 600" file.plist

# Add new key
$ /usr/libexec/PlistBuddy -c "Add :NewKey string 'value'" file.plist

# Add array item
$ /usr/libexec/PlistBuddy -c "Add :ProgramArguments: string '--newarg'" file.plist

# Delete key
$ /usr/libexec/PlistBuddy -c "Delete :UnwantedKey" file.plist

# Nested access
$ /usr/libexec/PlistBuddy -c "Print :EnvironmentVariables:PATH" file.plist

# Interactive mode
$ /usr/libexec/PlistBuddy file.plist
Command: Print
Command: Set :Label com.example.newname
Command: Save
Command: Exit

Creating Launch Agent Plists

Minimal Agent

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.minimal</string>
    <key>Program</key>
    <string>/usr/local/bin/myprogram</string>
</dict>
</plist>

Agent with Arguments

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.withargs</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/myprogram</string>
        <string>--config</string>
        <string>/etc/myconfig.conf</string>
        <string>--verbose</string>
    </array>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.full</string>

    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/myprogram</string>
    </array>

    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin</string>
        <key>HOME</key>
        <string>/Users/david</string>
    </dict>

    <key>WorkingDirectory</key>
    <string>/Users/david/project</string>

    <key>StandardOutPath</key>
    <string>/tmp/myprogram.stdout.log</string>

    <key>StandardErrorPath</key>
    <string>/tmp/myprogram.stderr.log</string>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>ThrottleInterval</key>
    <integer>10</integer>
</dict>
</plist>

Validation and Testing

Validate Syntax

# Check plist syntax
$ plutil -lint file.plist
file.plist: OK

# Detailed check
$ launchctl print gui/$(id -u)/com.example.myjob 2>&1 | head

Common Errors

Bad permissions:

# Agents must be owned by user
$ ls -la ~/Library/LaunchAgents/com.example.plist
-rw-r--r--  1 david  staff  500 Jan 15 10:00 com.example.plist

# Daemons must be owned by root
$ ls -la /Library/LaunchDaemons/com.example.plist
-rw-r--r--  1 root  wheel  500 Jan 15 10:00 com.example.plist

Missing Label:

launchctl: Error unloading: com.example.myjob: No such process

Invalid Program Path:

# Check path exists and is executable
$ ls -la /usr/local/bin/myprogram
$ file /usr/local/bin/myprogram

Editing Tips

Using Xcode

Xcode provides a plist editor:

  1. Open plist file in Xcode
  2. Visual editor shows keys and values
  3. Right-click to add/remove entries

Using Text Editor

For XML plists:

$ nano ~/Library/LaunchAgents/com.example.plist
# or
$ code ~/Library/LaunchAgents/com.example.plist

Convert Binary to Edit

# Convert to XML for editing
$ plutil -convert xml1 /path/to/file.plist

# Edit...

# Convert back to binary (optional, for performance)
$ plutil -convert binary1 /path/to/file.plist

Summary

Plist essentials:

ToolUse Case
plutilValidate, convert formats
defaultsRead/write app preferences
PlistBuddyComplex edits, scripting

Key points:

  • XML format is human-readable
  • Binary format is faster
  • Always validate after editing
  • Check permissions for launchd plists
  • Use proper data types

Creating Launch Agents and Daemons

This chapter walks through creating your own launchd jobs step by step, covering both user agents and system daemons with practical examples.

Agent vs Daemon: Which Do You Need?

RequirementUse
Run before any user logs inDaemon
Access user’s GUI or filesAgent
Run as rootDaemon
Run as current userAgent
System-wide scopeDaemon
Per-user scopeAgent

Creating a Launch Agent

Step 1: Create Your Script

$ mkdir -p ~/bin
$ cat > ~/bin/backup-documents.sh << 'EOF'
#!/bin/bash
# Simple backup script
BACKUP_DIR="$HOME/Backups/Documents"
mkdir -p "$BACKUP_DIR"
rsync -av --delete "$HOME/Documents/" "$BACKUP_DIR/"
echo "$(date): Backup completed" >> "$HOME/Backups/backup.log"
EOF
$ chmod +x ~/bin/backup-documents.sh

Step 2: Create the Plist

$ cat > ~/Library/LaunchAgents/com.example.backup.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.backup</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/david/bin/backup-documents.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>14</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/tmp/backup.stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/backup.stderr.log</string>
</dict>
</plist>
EOF

Step 3: Set Permissions

$ chmod 644 ~/Library/LaunchAgents/com.example.backup.plist
$ ls -la ~/Library/LaunchAgents/com.example.backup.plist
-rw-r--r--  1 david  staff  ...

Step 4: Load and Test

# Load the agent
$ launchctl load ~/Library/LaunchAgents/com.example.backup.plist

# Verify it's loaded
$ launchctl list | grep backup
-       0       com.example.backup

# Run immediately for testing
$ launchctl start com.example.backup

# Check output
$ cat /tmp/backup.stdout.log

Step 5: Unload if Needed

$ launchctl unload ~/Library/LaunchAgents/com.example.backup.plist

Creating a Launch Daemon

Daemons require root privileges and run system-wide.

Step 1: Create the Script

$ sudo mkdir -p /usr/local/bin
$ sudo tee /usr/local/bin/cleanup-system.sh << 'EOF'
#!/bin/bash
# System cleanup script
LOG="/var/log/cleanup.log"
echo "$(date): Starting cleanup" >> "$LOG"

# Clean old logs
find /var/log -name "*.log" -mtime +30 -delete 2>/dev/null

# Clean temp files
find /tmp -type f -atime +7 -delete 2>/dev/null

echo "$(date): Cleanup completed" >> "$LOG"
EOF
$ sudo chmod +x /usr/local/bin/cleanup-system.sh

Step 2: Create the Plist

$ sudo tee /Library/LaunchDaemons/com.example.cleanup.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.cleanup</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/cleanup-system.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>3</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/var/log/cleanup.stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/cleanup.stderr.log</string>
</dict>
</plist>
EOF

Step 3: Set Ownership and Permissions

$ sudo chown root:wheel /Library/LaunchDaemons/com.example.cleanup.plist
$ sudo chmod 644 /Library/LaunchDaemons/com.example.cleanup.plist

Step 4: Load

$ sudo launchctl load /Library/LaunchDaemons/com.example.cleanup.plist

Common Patterns

Keep-Alive Service

A service that should always be running:

<key>KeepAlive</key>
<true/>

<key>ThrottleInterval</key>
<integer>30</integer>

Watch for File Changes

Run when a file or directory changes:

<key>WatchPaths</key>
<array>
    <string>/path/to/watch</string>
</array>

Run at Login

<key>RunAtLoad</key>
<true/>

Scheduled Intervals

Every hour:

<key>StartInterval</key>
<integer>3600</integer>

Every day at midnight:

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>0</integer>
</dict>

Every Monday at 9 AM:

<key>StartCalendarInterval</key>
<dict>
    <key>Weekday</key>
    <integer>1</integer>
    <key>Hour</key>
    <integer>9</integer>
    <key>Minute</key>
    <integer>0</integer>
</dict>

Multiple times:

<key>StartCalendarInterval</key>
<array>
    <dict>
        <key>Hour</key>
        <integer>8</integer>
    </dict>
    <dict>
        <key>Hour</key>
        <integer>12</integer>
    </dict>
    <dict>
        <key>Hour</key>
        <integer>18</integer>
    </dict>
</array>

Environment Variables

<key>EnvironmentVariables</key>
<dict>
    <key>PATH</key>
    <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    <key>LANG</key>
    <string>en_US.UTF-8</string>
</dict>

Network-Dependent

Wait for network before running:

<key>KeepAlive</key>
<dict>
    <key>NetworkState</key>
    <true/>
</dict>

Practical Examples

Sync Script on Network Change

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.networksync</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/david/bin/sync.sh</string>
    </array>
    <key>WatchPaths</key>
    <array>
        <string>/Library/Preferences/SystemConfiguration</string>
    </array>
</dict>
</plist>

Process Queue Directory

Run when files appear in a directory:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.processor</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/david/bin/process-files.sh</string>
    </array>
    <key>QueueDirectories</key>
    <array>
        <string>/Users/david/incoming</string>
    </array>
</dict>
</plist>

Web Server Daemon

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.webserver</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/python3</string>
        <string>-m</string>
        <string>http.server</string>
        <string>8080</string>
    </array>
    <key>WorkingDirectory</key>
    <string>/var/www</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>

Debugging

Check Job Status

$ launchctl list | grep com.example
-       78      com.example.myjob
# PID "-" = not running, exit code 78 = error

View Logs

# Job-specific logs
$ cat /tmp/myjob.stderr.log

# System logs
$ log show --predicate 'subsystem == "com.apple.launchd"' --last 10m

# Specific job
$ log show --predicate 'process == "myscript"' --last 1h

Manual Test

# Run the program directly
$ /path/to/myscript.sh

# Check exit code
$ echo $?

Common Issues

ProblemSolution
Job won’t loadCheck plist syntax with plutil
Job loads but doesn’t runCheck Program path exists
Job runs but failsCheck logs, run manually
Permission deniedCheck script is executable
Wrong environmentAdd EnvironmentVariables

Summary

Creating launchd jobs:

  1. Choose type: Agent (user) or Daemon (system)
  2. Create script: Executable, tested manually
  3. Create plist: Valid XML, required keys
  4. Set permissions: 644, correct ownership
  5. Load: launchctl load
  6. Test: launchctl start, check logs
  7. Debug: View output, check status

launchctl Command Reference

launchctl is the command-line interface to launchd. Its syntax has evolved over macOS versions, with modern versions using a domain-based approach. This chapter covers both legacy and modern commands.

Command Overview

Legacy Commands (Still Supported)

launchctl load <plist>         # Load a job
launchctl unload <plist>       # Unload a job
launchctl start <label>        # Start a loaded job
launchctl stop <label>         # Stop a running job
launchctl list [label]         # List jobs
launchctl submit               # Submit job without plist
launchctl remove <label>       # Remove a submitted job

Modern Commands (macOS 10.10+)

launchctl bootstrap <domain> <plist>   # Load job
launchctl bootout <domain-target>      # Unload job
launchctl enable <domain-target>       # Enable job
launchctl disable <domain-target>      # Disable job
launchctl kickstart <domain-target>    # Force start
launchctl kill <signal> <domain-target># Send signal
launchctl print <domain-target>        # Show job info
launchctl print-cache                  # Show cache
launchctl print-disabled <domain>      # Show disabled jobs

Domains and Targets

Domain Types

DomainSyntaxDescription
systemsystemSystem services
useruser/<uid>User services
guigui/<uid>GUI session services
loginlogin/<uid>Login services
pidpid/<pid>Per-process services

Target Syntax

# Domain only
system
user/501
gui/501

# Domain + service
system/com.apple.mds
user/501/com.example.agent
gui/501/com.apple.Dock

Getting Your UID

$ id -u
501

# Or in commands
$ launchctl print gui/$(id -u)

Job Management

Loading Jobs

# Legacy (still works)
$ launchctl load ~/Library/LaunchAgents/com.example.agent.plist
$ sudo launchctl load /Library/LaunchDaemons/com.example.daemon.plist

# Modern
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.agent.plist
$ sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.daemon.plist

# Load and enable
$ launchctl load -w ~/Library/LaunchAgents/com.example.agent.plist
# -w writes enabled status to overrides database

Unloading Jobs

# Legacy
$ launchctl unload ~/Library/LaunchAgents/com.example.agent.plist

# Modern
$ launchctl bootout gui/$(id -u)/com.example.agent
$ sudo launchctl bootout system/com.example.daemon

# Unload and disable
$ launchctl unload -w ~/Library/LaunchAgents/com.example.agent.plist

Starting and Stopping

# Start a loaded job
$ launchctl start com.example.agent

# Stop a running job
$ launchctl stop com.example.agent

# Modern: Force start (even if conditions not met)
$ launchctl kickstart gui/$(id -u)/com.example.agent

# Force start and restart if running
$ launchctl kickstart -k gui/$(id -u)/com.example.agent

# Force start and print PID
$ launchctl kickstart -p gui/$(id -u)/com.example.agent

Enabling and Disabling

# Enable (will load at next boot/login)
$ launchctl enable user/$(id -u)/com.example.agent
$ sudo launchctl enable system/com.example.daemon

# Disable (won't load)
$ launchctl disable user/$(id -u)/com.example.agent
$ sudo launchctl disable system/com.example.daemon

# Check disabled status
$ launchctl print-disabled user/$(id -u)

Listing and Inspecting

List Jobs

# List all loaded jobs
$ launchctl list
PID     Status  Label
-       0       com.apple.AMPArtworkAgent
1234    0       com.apple.Dock
...

# Filter by pattern
$ launchctl list | grep com.apple
$ launchctl list | grep -E "^-"  # Not running

# Get specific job
$ launchctl list com.apple.Dock
{
    "LimitLoadToSessionType" = "Aqua";
    "Label" = "com.apple.Dock";
    "OnDemand" = false;
    "LastExitStatus" = 0;
    "PID" = 1234;
    "Program" = "/System/Library/CoreServices/Dock.app/Contents/MacOS/Dock";
};

Detailed Information

# Print job details (modern)
$ launchctl print gui/$(id -u)/com.apple.Dock
com.apple.Dock = {
    active count = 1
    path = /System/Library/LaunchAgents/com.apple.Dock.plist
    state = running
    program = /System/Library/CoreServices/Dock.app/Contents/MacOS/Dock
    ...
}

# Print entire domain
$ launchctl print gui/$(id -u) | head -50

# Print system domain (requires sudo)
$ sudo launchctl print system | head -50

Process Information

# Get info about a specific service
$ sudo launchctl procinfo <pid>

# Blame: which job launched a process
$ launchctl blame gui/$(id -u)/com.example.agent

Environment and Configuration

Setting Environment Variables

# Set environment variable for GUI session
$ launchctl setenv MY_VAR "my_value"

# Get environment variable
$ launchctl getenv MY_VAR

# Unset environment variable
$ launchctl unsetenv MY_VAR

# Note: These affect new processes, not already running ones

Managing Resources

# Set resource limits
$ launchctl limit
    cpu         unlimited      unlimited
    filesize    unlimited      unlimited
    data        unlimited      unlimited
    stack       8388608        67104768
    core        0              unlimited
    rss         unlimited      unlimited
    memlock     unlimited      unlimited
    maxproc     2784           4176
    maxfiles    256            unlimited

# Modify limit (for session)
$ launchctl limit maxfiles 65536 200000

Signals and Control

Sending Signals

# Kill with signal
$ launchctl kill SIGTERM gui/$(id -u)/com.example.agent
$ launchctl kill SIGKILL gui/$(id -u)/com.example.agent
$ launchctl kill SIGHUP gui/$(id -u)/com.example.agent

# Signal numbers also work
$ launchctl kill 15 gui/$(id -u)/com.example.agent

Service Control

# Reboot (requires authorization)
$ sudo launchctl reboot

# Shutdown
$ sudo launchctl shutdown

# Single-user mode
$ sudo launchctl reboot -s

Debugging

Debug Mode

# Enable debug mode for job
$ launchctl debug gui/$(id -u)/com.example.agent

# Run with additional output
$ launchctl debug gui/$(id -u)/com.example.agent -- /path/to/program

Examining Errors

# Check why job failed
$ launchctl error <error_number>
# Example:
$ launchctl error 78
78: Function not implemented

# View launchd log
$ log show --predicate 'subsystem == "com.apple.launchd"' --last 1h

# View specific job
$ log show --predicate 'process == "myprocess"' --last 1h

Analyzing

# Export service to XML
$ launchctl dumpstate > /tmp/launchd-state.txt
$ launchctl dumpjpcategory > /tmp/jpcategory.txt

# Analyze job
$ sudo launchctl plist /Library/LaunchDaemons/com.example.plist

Homebrew Services

Homebrew provides a wrapper for launchctl:

# Start service (loads and starts)
$ brew services start postgresql@14

# Stop service
$ brew services stop postgresql@14

# Restart
$ brew services restart postgresql@14

# List services
$ brew services list

# Run once (doesn't persist across reboot)
$ brew services run postgresql@14

Behind the scenes, brew services creates and manages plists in ~/Library/LaunchAgents/.

Quick Reference

TaskCommand
Load agentlaunchctl load ~/Library/LaunchAgents/X.plist
Load daemonsudo launchctl load /Library/LaunchDaemons/X.plist
Unloadlaunchctl unload /path/to/plist
Startlaunchctl start label
Stoplaunchctl stop label
List alllaunchctl list
List onelaunchctl list label
Job infolaunchctl print gui/$(id -u)/label
Enablelaunchctl enable user/$(id -u)/label
Disablelaunchctl disable user/$(id -u)/label
Force startlaunchctl kickstart gui/$(id -u)/label
Set envlaunchctl setenv VAR value
Get envlaunchctl getenv VAR

Summary

launchctl provides comprehensive control over launchd:

  • Legacy commands still work and are simpler
  • Modern commands provide more control via domains
  • Domains organize services by scope (system, user, gui)
  • Enable/disable controls whether jobs load
  • Kickstart forces immediate execution
  • Use brew services for Homebrew packages

Process Management: CLI vs GUI

macOS offers both command-line tools and graphical applications for managing processes. Understanding both approaches helps you choose the right tool for each situation and troubleshoot process-related issues effectively.

Command-Line Process Tools

ps - Process Status

# BSD syntax (common on macOS)
$ ps aux
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root     1   0.0  0.1 410148544  18624   ??  Ss   Mon10AM   0:15.23 /sbin/launchd
david  501   0.0  0.2 419826688  35712   ??  S    Mon10AM   0:05.67 /usr/libexec/...

# POSIX syntax
$ ps -ef
  UID   PID  PPID   C STIME   TTY           TIME CMD
    0     1     0   0 Mon10AM ??         0:15.23 /sbin/launchd

# Current user's processes
$ ps -u $USER

# Process tree (requires pstree)
$ brew install pstree
$ pstree

Key ps Columns

ColumnMeaning
PIDProcess ID
PPIDParent Process ID
%CPUCPU usage percentage
%MEMMemory usage percentage
VSZVirtual memory size
RSSResident set size (physical memory)
TTControlling terminal
STATProcess state
TIMECPU time consumed

Process States

StateMeaning
RRunning
SSleeping (interruptible)
UUninterruptible sleep
IIdle
TStopped
ZZombie

top - Real-Time Process Monitor

# Start top
$ top

# Sort by CPU (default)
$ top -o cpu

# Sort by memory
$ top -o mem

# Show specific user
$ top -U david

# Non-interactive (for scripts)
$ top -l 1 -n 10 -stats pid,command,cpu

htop - Enhanced top

$ brew install htop
$ htop
# Arrow keys to navigate, F9 to kill, q to quit

Signals and kill

# List signals
$ kill -l

# Send SIGTERM (graceful shutdown)
$ kill <pid>
$ kill -15 <pid>
$ kill -TERM <pid>

# Send SIGKILL (force kill)
$ kill -9 <pid>
$ kill -KILL <pid>

# Send SIGHUP (reload configuration)
$ kill -HUP <pid>

# Kill by name
$ pkill -f "process_name"
$ killall process_name

Finding Processes

# Find by name
$ pgrep -l Safari
1234 Safari

# Find by name (full command line)
$ pgrep -fl python

# Find what's using a port
$ lsof -i :8080
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
python  12345 david  4u   IPv4 0x1234567890      0t0  TCP *:http-alt (LISTEN)

# Find what's using a file
$ lsof /path/to/file

# Find open files by process
$ lsof -p <pid>

# Find processes using network
$ lsof -i

Process Priority

# Run with lower priority (higher nice value)
$ nice -n 10 ./slow_script.sh

# Change running process priority
$ renice 10 -p <pid>

# Run with higher priority (requires root)
$ sudo nice -n -10 ./important_script.sh

Activity Monitor

Activity Monitor is macOS’s graphical process manager, found in /Applications/Utilities/Activity Monitor.app.

Views

TabShows
CPUProcessor usage
MemoryRAM usage
EnergyBattery impact
DiskI/O operations
NetworkNetwork activity

Key Features

Process List:

  • Sort by any column
  • Search/filter processes
  • Show all processes vs user processes

Process Information (double-click process):

  • Open files and ports
  • Memory map
  • Statistics
  • Parent process

Actions (select process, use toolbar or View menu):

  • Quit: Sends SIGTERM
  • Force Quit: Sends SIGKILL
  • Sample: Creates stack trace

Activity Monitor from CLI

# Open Activity Monitor
$ open -a "Activity Monitor"

# Get same data as Activity Monitor
$ top -l 1 -n 0 -stats pid,command,cpu,mem

# Sample a process (like Activity Monitor's Sample)
$ sample <pid> 5  # 5 second sample

Comparing CLI and GUI

TaskCLIGUI
Quick process listps auxActivity Monitor
Real-time monitoringtop, htopActivity Monitor
Kill processkill <pid>Select → Quit
Find resource hogtop -o cpuSort by CPU column
Analyze processlsof -p <pid>Double-click → Open Files
Sample/profilesample <pid>View → Sample
System overviewvm_stat, iostatActivity Monitor tabs

Process Monitoring Tools

vm_stat - Virtual Memory

$ vm_stat
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free:                               41203.
Pages active:                            385918.
Pages inactive:                          347108.
Pages speculative:                        11276.
Pages wired down:                         96482.
...

# Continuous monitoring
$ vm_stat 1  # Every 1 second

iostat - I/O Statistics

$ iostat
              disk0       cpu    load average
    KB/t  tps  MB/s  us sy id   1m   5m   15m
   25.67   12  0.30   3  2 95  2.07 2.15 2.10

# Continuous
$ iostat 1

fs_usage - File System Activity

# Monitor file system calls (requires root)
$ sudo fs_usage

nettop - Network Top

# Monitor network connections per process
$ nettop

Practical Examples

Find and Kill Runaway Process

# Find high CPU process
$ ps aux --sort=-%cpu | head -5

# Or interactively
$ top
# Press 'q' when found

# Kill it
$ kill <pid>
# If unresponsive
$ kill -9 <pid>

Find Memory Hog

$ ps aux --sort=-%mem | head -10

# Or in top
$ top -o mem

Monitor Specific Process

# Watch a process
$ while true; do ps -p <pid> -o %cpu,%mem,rss; sleep 1; done

# Or use watch
$ brew install watch
$ watch -n 1 "ps -p <pid> -o %cpu,%mem,rss"

Debug Hanging Process

# Sample the process
$ sample <pid> 10 -f /tmp/sample.txt

# View open files
$ lsof -p <pid>

# View system calls (requires SIP adjustment)
$ sudo dtruss -p <pid>

Summary

Process management tools:

ToolPurpose
psSnapshot of processes
topReal-time monitoring
htopEnhanced top
killSend signals to processes
pgrep/pkillFind/kill by name
lsofOpen files and ports
nice/reniceProcess priority
sampleStack trace sampling
Activity MonitorGUI all-in-one

Best practices:

  • Use top or Activity Monitor for interactive monitoring
  • Use ps for scripting and one-time checks
  • Always try kill before kill -9
  • Use lsof to find what’s using resources

Background Task Scheduling

Scheduling tasks on macOS can be done through launchd (the modern macOS way) or cron (the traditional Unix way). Both have their place, but launchd is preferred for most use cases on macOS.

launchd Scheduling

Interval-Based Scheduling

Run every N seconds:

<key>StartInterval</key>
<integer>300</integer>  <!-- Every 5 minutes -->
<key>StartInterval</key>
<integer>3600</integer>  <!-- Every hour -->

Calendar-Based Scheduling

Using StartCalendarInterval for specific times:

<!-- Every day at 3:30 AM -->
<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>3</integer>
    <key>Minute</key>
    <integer>30</integer>
</dict>
<!-- Every Monday at 9 AM -->
<key>StartCalendarInterval</key>
<dict>
    <key>Weekday</key>
    <integer>1</integer>
    <key>Hour</key>
    <integer>9</integer>
</dict>
<!-- First of every month at midnight -->
<key>StartCalendarInterval</key>
<dict>
    <key>Day</key>
    <integer>1</integer>
    <key>Hour</key>
    <integer>0</integer>
</dict>

Calendar Interval Keys

KeyRangeDescription
Month1-12Month of year
Day1-31Day of month
Weekday0-7Day of week (0 and 7 = Sunday)
Hour0-23Hour of day
Minute0-59Minute of hour

Multiple Schedules

<key>StartCalendarInterval</key>
<array>
    <dict>
        <key>Hour</key>
        <integer>8</integer>
    </dict>
    <dict>
        <key>Hour</key>
        <integer>12</integer>
    </dict>
    <dict>
        <key>Hour</key>
        <integer>18</integer>
    </dict>
</array>

Complete Scheduled Agent Example

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.scheduled-backup</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/backup.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>2</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/var/log/backup.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/backup.err</string>
</dict>
</plist>

cron (Traditional Unix)

cron still works on macOS but is somewhat deprecated in favor of launchd.

cron Basics

# Edit crontab
$ crontab -e

# List crontab
$ crontab -l

# Remove crontab
$ crontab -r

cron Syntax

* * * * * command
│ │ │ │ │
│ │ │ │ └── Day of week (0-7, Sunday=0 or 7)
│ │ │ └──── Month (1-12)
│ │ └────── Day of month (1-31)
│ └──────── Hour (0-23)
└────────── Minute (0-59)

cron Examples

# Every minute
* * * * * /path/to/script.sh

# Every hour
0 * * * * /path/to/script.sh

# Every day at 2 AM
0 2 * * * /path/to/script.sh

# Every Monday at 9 AM
0 9 * * 1 /path/to/script.sh

# Every 15 minutes
*/15 * * * * /path/to/script.sh

# First of month at midnight
0 0 1 * * /path/to/script.sh

# Weekdays at 6 PM
0 18 * * 1-5 /path/to/script.sh

Full Crontab Example

$ crontab -e
# Add:
SHELL=/bin/zsh
PATH=/opt/homebrew/bin:/usr/bin:/bin

# Backup at 2 AM daily
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

# Cleanup every Sunday at 3 AM
0 3 * * 0 /usr/local/bin/cleanup.sh

# Check disk space every hour
0 * * * * df -h | mail -s "Disk Report" admin@example.com

cron Limitations on macOS

  1. No on-demand loading: Unlike launchd, cron always runs
  2. No power management: Doesn’t integrate with sleep/wake
  3. No GUI access: Can’t interact with user session
  4. Limited logging: Must redirect output manually

at Command

For one-time scheduled tasks:

# Schedule command for specific time
$ at 2:30 PM
at> /usr/local/bin/myscript.sh
at> <Ctrl+D>
job 1 at Tue Jan 16 14:30:00 2024

# Schedule for specific date
$ at 2:30 PM Jan 20
at> /usr/local/bin/myscript.sh
at> <Ctrl+D>

# Schedule relative time
$ at now + 1 hour
$ at now + 30 minutes
$ at midnight
$ at noon tomorrow

# List pending jobs
$ atq

# Remove job
$ atrm <job_number>

Enabling atrun

atrun must be enabled for at to work:

$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.atrun.plist

launchd vs cron Comparison

Featurelaunchdcron
On-demand loadingYesNo
Power managementYesNo
Resource limitsYesNo
GUI session accessYes (agents)No
Path watchingYesNo
Network conditionsYesNo
LoggingUnified logManual
Complex schedulesStartCalendarIntervalMore flexible syntax

When to Use Which

Use launchd when:

  • Building macOS-native services
  • Need on-demand or event-based execution
  • Want integration with power management
  • Need to access GUI session

Use cron when:

  • Porting scripts from Linux
  • Simple time-based scheduling
  • More familiar with cron syntax
  • Writing cross-platform scripts

Practical Example: Migration from cron to launchd

cron entry:

0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Equivalent launchd plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.backup</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/backup.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>2</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/var/log/backup.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/backup.log</string>
</dict>
</plist>

Summary

Task scheduling options:

MethodBest For
launchdMost macOS scheduling needs
cronSimple, cross-platform scripts
atOne-time future execution

launchd advantages:

  • Better integration with macOS
  • On-demand and event-based triggers
  • Power management awareness
  • Unified logging

For new projects on macOS, prefer launchd. Use cron for compatibility or when its syntax is more convenient.

XPC Services and IPC

XPC (Cross-Process Communication) is Apple’s modern IPC framework, built on top of Mach ports. While application developers use XPC extensively, understanding it helps explain how macOS services communicate and how to troubleshoot issues.

IPC Mechanisms on macOS

MechanismUse CaseAPI Level
XPCModern Apple IPCHigh-level
Mach portsKernel-level IPCLow-level
Unix socketsNetwork-style IPCPOSIX
PipesParent-child communicationPOSIX
Shared memoryHigh-performance data sharingPOSIX

Understanding XPC

What XPC Provides

XPC offers:

  • Type-safe message passing: Dictionaries, arrays, strings, data
  • Crash isolation: Service crashes don’t bring down client
  • Resource management: Automatic cleanup
  • Security: Entitlements and sandboxing integration
  • Launchd integration: On-demand service activation

XPC Service Types

TypeDescription
XPC ServiceBundled in application, private
Launch DaemonSystem-wide, runs as root
Launch AgentPer-user, runs as user
Mach ServiceRegistered with launchd

XPC from Command Line

Viewing XPC Services

# List XPC services in an application
$ ls /Applications/Safari.app/Contents/XPCServices/
com.apple.Safari.SearchHelper.xpc
com.apple.Safari.SandboxBroker.xpc
...

# View XPC service info
$ plutil -p /Applications/Safari.app/Contents/XPCServices/com.apple.Safari.SearchHelper.xpc/Contents/Info.plist

XPC and launchd

# List Mach services (registered with launchd)
$ launchctl print system | grep "mach services"

# Check specific service
$ launchctl print system/com.apple.metadata.mds

Debugging XPC

# View XPC activity in logs
$ log show --predicate 'subsystem == "com.apple.xpc"' --last 10m

# Activity related to specific service
$ log show --predicate 'process == "com.apple.xpc.serviceproxy"' --last 1h

Mach Ports

Mach ports underlie all macOS IPC:

# List Mach ports for a process
$ sudo lsmp -p <pid>
Process (12345) : Safari
  name      ipc-object    rights     flags   boost  reqs...
---------   ----------    ------     -----   -----  ----
...

# View Mach port rights
$ sudo lsmp -a | head -50

Unix IPC Tools

Named Pipes (FIFOs)

# Create named pipe
$ mkfifo /tmp/mypipe

# Terminal 1: Read from pipe
$ cat /tmp/mypipe

# Terminal 2: Write to pipe
$ echo "Hello" > /tmp/mypipe

# Cleanup
$ rm /tmp/mypipe

Unix Domain Sockets

# List Unix sockets
$ netstat -f unix | head -20

# Find socket files
$ find /var/run -name "*.sock" 2>/dev/null

Shared Memory

# List shared memory segments
$ ipcs -m

# Detailed info
$ ipcs -a

Practical Applications

Checking Service Communication

# See what an app is connecting to
$ lsof -c Safari | grep -E "sock|unix"

# Network connections by process
$ lsof -i -P | grep Safari

Diagnosing XPC Issues

Common XPC problems:

  1. Service not launching

    $ log show --predicate 'subsystem == "com.apple.xpc" AND eventMessage CONTAINS "error"' --last 1h
    
  2. Entitlement issues

    $ codesign -d --entitlements :- /path/to/app
    
  3. Sandbox violations

    $ log show --predicate 'eventMessage CONTAINS "sandbox"' --last 1h
    

Creating Simple IPC

For shell scripts, use simple mechanisms:

# Using a socket with nc
# Server:
$ nc -l 8080

# Client:
$ echo "message" | nc localhost 8080

# Using files (simple but not ideal)
$ echo "message" > /tmp/ipc_file
$ inotifywait -e modify /tmp/ipc_file  # Linux
# macOS alternative: use fswatch
$ fswatch /tmp/ipc_file

Security Considerations

XPC Security Features

  1. Code signing validation: XPC verifies code signatures
  2. Entitlements: Services can require specific entitlements
  3. Sandboxing: XPC respects sandbox boundaries
  4. Audit tokens: Services can verify client identity

Checking Security

# Verify code signature
$ codesign -vvv /path/to/service

# Check entitlements
$ codesign -d --entitlements :- /path/to/service

# Check sandbox profile
$ sandbox-exec -n /path/to/profile /bin/ls  # Test sandbox

For Developers

XPC services are created in Xcode. Key components:

  1. XPC Service Target: Bundle containing the service
  2. Info.plist: Service configuration
  3. NSXPCConnection: Client-side API
  4. Protocol: Defines service interface

Example service registration in launchd plist:

<key>MachServices</key>
<dict>
    <key>com.example.myservice</key>
    <true/>
</dict>

Summary

IPC on macOS:

MethodComplexityUse Case
XPCHighModern Apple development
Mach portsVery highKernel/system services
Unix socketsMediumNetwork-style IPC
Named pipesLowSimple shell scripts
Files/fswatchLowQuick prototypes

Key points:

  • XPC is Apple’s preferred modern IPC
  • Mach ports are the underlying mechanism
  • Unix IPC still works for traditional needs
  • Use log show and lsof for debugging
  • Security is built into XPC

For most users, understanding XPC helps debug issues and understand how macOS services interact. Actual XPC development requires Xcode and deeper study.

Comparing launchd to systemd and init

If you’re coming from Linux, you’re probably familiar with systemd or traditional init scripts. This chapter maps those concepts to launchd, helping you translate your knowledge to macOS.

Architecture Comparison

FeaturelaunchdsystemdSysVinit
PID 1YesYesYes
On-demand activationYesYesNo
Socket activationYesYesVia inetd
Timer unitsVia plistTimer unitsVia cron
ConfigurationXML plistsINI-like unitsShell scripts
Dependency managementImplicitExplicitManual
Cgroup integrationNoYesNo
Container supportNoYesNo

Concept Mapping

Service Files

systemd (.service file):

[Unit]
Description=My Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/myservice
Restart=always

[Install]
WantedBy=multi-user.target

launchd (.plist file):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.myservice</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/myservice</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>

Command Translation

Tasksystemdlaunchd
Start servicesystemctl start foolaunchctl start foo
Stop servicesystemctl stop foolaunchctl stop foo
Enable at bootsystemctl enable foolaunchctl load -w
Disablesystemctl disable foolaunchctl unload -w
Check statussystemctl status foolaunchctl list foo
List servicessystemctl list-unitslaunchctl list
View logsjournalctl -u foolog show --predicate 'process=="foo"'
Reload configsystemctl daemon-reloadAutomatic

Service Locations

Typesystemdlaunchd
System services/etc/systemd/system//Library/LaunchDaemons/
User services~/.config/systemd/user/~/Library/LaunchAgents/
Vendor services/usr/lib/systemd/system//System/Library/LaunchDaemons/

Key Differences

Dependencies

systemd: Explicit dependencies with After=, Requires=, Wants=

[Unit]
After=network.target postgresql.service
Requires=postgresql.service

launchd: Implicit dependencies via on-demand activation

<!-- launchd handles dependencies via on-demand loading -->
<!-- No explicit dependency declaration -->

Service Types

systemd has multiple service types:

  • simple: Default, main process
  • forking: Forks and exits
  • oneshot: Runs once
  • notify: Uses sd_notify
  • dbus: D-Bus activated

launchd determines type implicitly:

  • RunAtLoad: Starts immediately
  • KeepAlive: Respawns if exits
  • StartInterval: Periodic
  • WatchPaths: Event-triggered

Restart Policies

systemd:

Restart=always
RestartSec=10
StartLimitBurst=5

launchd:

<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>

Environment Variables

systemd:

Environment="VAR1=value1" "VAR2=value2"
EnvironmentFile=/etc/default/myservice

launchd:

<key>EnvironmentVariables</key>
<dict>
    <key>VAR1</key>
    <string>value1</string>
    <key>VAR2</key>
    <string>value2</string>
</dict>

Logging

systemd: Integrated with journald

$ journalctl -u myservice -f

launchd: Uses unified logging or file redirection

$ log show --predicate 'process == "myservice"' --last 1h
# Or configure StandardOutPath/StandardErrorPath

Migration Guide

From systemd to launchd

  1. Service name: Use reverse-domain notation (com.example.service)
  2. ExecStart: Use ProgramArguments array
  3. Restart=always: Use KeepAlive
  4. After=network.target: Remove (launchd handles implicitly)
  5. Environment: Use EnvironmentVariables dict
  6. Install section: Use RunAtLoad or explicit loading

Common Patterns

Simple daemon:

<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>

Periodic task (like systemd timer):

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>3</integer>
</dict>

Socket activated:

<key>Sockets</key>
<dict>
    <key>Listeners</key>
    <dict>
        <key>SockServiceName</key>
        <string>8080</string>
    </dict>
</dict>

Traditional init Comparison

For users familiar with SysVinit:

SysVinitlaunchd Equivalent
/etc/init.d/foo startlaunchctl start foo
/etc/init.d/foo stoplaunchctl stop foo
chkconfig foo onlaunchctl load -w
update-rc.d foo defaultsPlace plist in LaunchDaemons
/etc/rc.d/rc.localUse Launch Agent/Daemon

What launchd Doesn’t Have

Features in systemd without direct launchd equivalents:

  • Cgroups: No container-style resource isolation
  • Slice/scope units: No hierarchical resource management
  • Socket/path/timer as separate units: All in one plist
  • Templates: No parameterized service files
  • Drop-in directories: No .d override directories
  • Portable services: No standardized service images

What launchd Has That Others Don’t

  • Tight GUI integration: Agents can interact with user session
  • Power management awareness: Respects sleep/wake
  • Application bundle integration: Services in app bundles
  • On-demand loading: More aggressive than systemd
  • XPC integration: Modern IPC framework

Summary

Translation guide:

Conceptsystemdlaunchd
Service definition.service.plist
Service managersystemctllaunchctl
LoggingjournaldUnified log
Timers.timer unitsStartCalendarInterval
Sockets.socket unitsSockets dict in plist
User servicesuser unitsLaunchAgents
System servicessystem unitsLaunchDaemons

Key mindset shifts:

  • launchd prefers on-demand over explicit dependencies
  • Configuration is XML instead of INI
  • Domains (system/user/gui) replace targets
  • Unified logging replaces journald
  • Less explicit control, more automatic management

Both are capable service managers; the differences are largely philosophical and syntactic.

Unix Commands on macOS

If you’ve used Linux, you already know the core Unix commands: ls, grep, sed, find, awk. The good news is these commands exist on macOS. The challenging news is they often behave differently. macOS ships with BSD versions of these tools, while Linux uses GNU versions. These differences will trip you up until you understand them.

The BSD Heritage

macOS descends from BSD Unix, specifically FreeBSD via NeXTSTEP. This heritage means:

# Check the version of common tools
$ ls --version
ls: illegal option -- -
usage: ls [-ABCFGHILOPRSTUWabcdefghiklmnopqrstuvwxy1%,] ...

$ /bin/ls --version  # BSD ls doesn't have --version
ls: illegal option -- -

# On Linux, this works:
$ ls --version
ls (GNU coreutils) 9.1

The BSD tools are older, follow stricter POSIX standards, and often have fewer features than their GNU counterparts. But they’re also leaner and more portable.

Why It Matters

Consider this common task—in-place editing with sed:

# On Linux (GNU sed)
$ sed -i 's/foo/bar/g' file.txt

# On macOS (BSD sed) - same command fails
$ sed -i 's/foo/bar/g' file.txt
sed: 1: "file.txt": invalid command code f

# The BSD way
$ sed -i '' 's/foo/bar/g' file.txt

BSD sed requires an argument after -i (the backup extension), even if it’s empty. This single difference breaks countless scripts when moving from Linux to macOS.

What You’ll Learn in This Part

BSD vs GNU: The Command Divide explains the fundamental differences between BSD and GNU command-line tools, with specific examples of where they diverge.

Essential macOS-Specific Commands covers commands unique to macOS like pbcopy, open, mdfind, say, and defaults—tools with no direct Linux equivalent.

Installing and Using GNU Coreutils shows how to install GNU versions via Homebrew while keeping BSD tools available, giving you the best of both worlds.

Text Processing: sed, awk, and grep provides a detailed comparison of text processing tools, showing equivalent commands for BSD and GNU versions.

File Operations and Manipulation covers file commands like cp, mv, rm, and macOS-specific tools like ditto, including extended attribute handling.

Networking Commands explains network diagnostics and configuration from the command line, noting what’s different from Linux.

System Information Commands shows how to query system details using macOS-specific commands like system_profiler, sw_vers, and sysctl.

Writing Portable Shell Scripts teaches techniques for writing scripts that work on both macOS and Linux without modification.

Quick Comparison

Here’s a taste of what differs between macOS and Linux:

TaskmacOS (BSD)Linux (GNU)
In-place sedsed -i '' 's/a/b/'sed -i 's/a/b/'
Extended regex in grepgrep -Egrep -E or grep -P
Copy to clipboardpbcopyxclip or xsel
Open file with default appopen file.pdfxdg-open file.pdf
Find files by contentmdfind "query"locate or find + grep
Colored lsls -Gls --color
Date formattingdate -j -fdate -d
Network interfacesifconfig, networksetupip addr
Service managementlaunchctlsystemctl

The Solutions

You have several approaches:

  1. Learn both: Know the BSD and GNU syntax for common operations
  2. Install GNU tools: Use Homebrew to get GNU coreutils
  3. Write portable scripts: Use POSIX-compatible syntax that works everywhere
  4. Use aliases: Create aliases that normalize behavior across platforms

Most experienced macOS users combine all four approaches. The following chapters show you how.

Checking What You Have

See what versions of tools are installed:

# Default tools are in /usr/bin
$ which sed
/usr/bin/sed

$ which grep
/usr/bin/grep

# After installing GNU tools via Homebrew
$ which gsed  # GNU sed
/opt/homebrew/bin/gsed

$ which ggrep  # GNU grep
/opt/homebrew/bin/ggrep

Check if you have Homebrew’s GNU tools:

$ brew list | grep -E '^(coreutils|gnu-sed|grep|gawk)$'
coreutils
gnu-sed
grep
gawk

A Note on Examples

Throughout this part, examples show both BSD (macOS default) and GNU (Linux-style) syntax when they differ. Commands are tested on:

  • macOS Sonoma 14.x with BSD tools
  • macOS with GNU coreutils via Homebrew

When a command works the same on both platforms, only one version is shown.

BSD vs GNU: The Command Divide

The same command name doesn’t mean the same behavior. macOS uses BSD-derived tools, while Linux uses GNU tools. These two families share common ancestry but have diverged significantly. Understanding these differences is essential for anyone working across both platforms.

Historical Context

The split dates back to the 1980s:

  • BSD tools: Developed at UC Berkeley, focused on simplicity and POSIX compliance
  • GNU tools: Created by the Free Software Foundation, adding many extensions

macOS inherited BSD tools through its NeXTSTEP lineage, while Linux adopted GNU tools because they were freely available and feature-rich.

The Major Differences

sed: In-Place Editing

The most commonly encountered difference:

# GNU sed (Linux)
$ sed -i 's/old/new/g' file.txt

# BSD sed (macOS) - requires backup extension argument
$ sed -i '' 's/old/new/g' file.txt    # No backup
$ sed -i '.bak' 's/old/new/g' file.txt  # Creates file.txt.bak

BSD sed’s -i flag requires an argument specifying the backup file extension. An empty string '' means no backup. Omitting this argument entirely causes the substitution pattern to be interpreted as the backup extension—hence the confusing error messages.

More sed differences:

# Insert a newline (GNU sed)
$ echo "hello" | sed 's/$/\n/'

# Insert a newline (BSD sed) - \n doesn't work
$ echo "hello" | sed 's/$/\'$'\n''/'   # Using $'...' quoting
# Or use a literal newline:
$ echo "hello" | sed 's/$/\
/'

# Case-insensitive matching (GNU sed)
$ sed 's/foo/bar/gi' file.txt

# BSD sed doesn't support the 'i' flag
# Use tr or awk instead, or install GNU sed

grep: Regular Expression Flavors

# Perl-compatible regex (GNU grep only)
$ grep -P '\d{3}-\d{4}' file.txt    # Match phone numbers
grep: invalid option -- P

# BSD grep doesn't have -P flag
# Use extended regex instead
$ grep -E '[0-9]{3}-[0-9]{4}' file.txt

The -P flag for Perl-compatible regular expressions is a GNU extension. BSD grep supports -E (extended) and -G (basic) regex only.

Other grep differences:

# GNU grep colors matches by default (usually via alias)
$ grep --color=auto pattern file

# BSD grep requires explicit color flag
$ grep --color pattern file    # Works, but not default

# Line buffering (GNU)
$ tail -f log.txt | grep --line-buffered error

# BSD grep has --line-buffered too (one area of compatibility)
$ tail -f log.txt | grep --line-buffered error

ls: Display Options

# Colored output (GNU)
$ ls --color=auto

# Colored output (BSD/macOS)
$ ls -G

# Human-readable sizes (both support -h)
$ ls -lh

# Sort by modification time (both support -t)
$ ls -lt

# GNU-specific options that BSD lacks:
$ ls --group-directories-first  # BSD: illegal option
$ ls --time-style=long-iso      # BSD: illegal option

date: Format and Parsing

Date handling differs dramatically:

# Parse a date string (GNU)
$ date -d "2024-01-15" +%s
1705276800

# Parse a date string (BSD)
$ date -j -f "%Y-%m-%d" "2024-01-15" +%s
1705276800

# Show date N days ago (GNU)
$ date -d "7 days ago"
Mon Jan  8 10:00:00 PST 2024

# Show date N days ago (BSD)
$ date -v-7d
Mon Jan  8 10:00:00 PST 2024

# Show date N days from now (GNU)
$ date -d "+7 days"

# Show date N days from now (BSD)
$ date -v+7d

# Relative dates with GNU
$ date -d "next friday"
$ date -d "last month"

# BSD doesn't support natural language dates

BSD date uses -j (don’t set the date) with -f (input format), while GNU date uses -d for parsing date strings.

cp, mv, rm: Progress and Verbosity

# Show progress (GNU cp)
$ cp --progress large_file.iso /destination/

# BSD cp has no --progress flag
# Use rsync instead on macOS
$ rsync --progress large_file.iso /destination/

# Verbose mode (both support -v)
$ cp -v source dest
$ mv -v old new
$ rm -v file

# Interactive mode (both support -i)
$ rm -i file
remove file? y

find: Expression Differences

# Delete found files (GNU)
$ find . -name "*.tmp" -delete

# BSD also supports -delete (this is compatible)
$ find . -name "*.tmp" -delete

# Regex matching (GNU - default is Emacs regex)
$ find . -regex ".*\.txt"

# BSD requires explicit regex type
$ find . -E -regex ".*\.txt"    # -E for extended regex

# Time-based search differs:
# GNU: -mtime uses 24-hour periods
# BSD: -mtime also uses 24-hour periods (compatible)

# But modification time in minutes:
# Both support -mmin (compatible)
$ find . -mmin -60    # Modified in last 60 minutes

xargs: Null Delimiter

# Handle filenames with spaces (GNU)
$ find . -name "*.txt" -print0 | xargs -0 rm

# BSD also supports -0 (compatible)
$ find . -name "*.txt" -print0 | xargs -0 rm

# Replace string (GNU)
$ echo "file.txt" | xargs -I {} cp {} {}.bak

# BSD also supports -I (compatible)
$ echo "file.txt" | xargs -I {} cp {} {}.bak

# Parallel execution (GNU)
$ find . -name "*.jpg" -print0 | xargs -0 -P 4 convert

# BSD xargs doesn't have -P
# Use parallel or GNU xargs

sort: Numeric and Version Sorting

# Version sort (GNU only)
$ printf "1.10\n1.2\n1.1" | sort -V
1.1
1.2
1.10

# BSD sort doesn't have -V
$ printf "1.10\n1.2\n1.1" | sort -V
sort: invalid option -- V

# Human numeric sort (GNU only)
$ du -h * | sort -h
1K    small.txt
10M   medium.txt
2G    large.txt

# BSD sort doesn't have -h
# Workaround: use plain numeric sort on raw bytes
$ du -k * | sort -n

cut: Character vs Byte

# Cut by character (mostly compatible)
$ echo "hello" | cut -c1-3
hel

# Cut by field (compatible)
$ echo "a:b:c" | cut -d: -f2
b

# Complement (GNU only)
$ echo "a:b:c" | cut -d: --complement -f2
a:c

# BSD cut doesn't have --complement

head and tail: Line Counts

# First N lines (compatible)
$ head -n 10 file.txt
$ head -10 file.txt      # Shorthand works on both

# Last N lines (compatible)
$ tail -n 10 file.txt
$ tail -10 file.txt

# All but last N lines (GNU)
$ head -n -5 file.txt    # All but last 5

# BSD head doesn't support negative counts
$ head -n -5 file.txt
head: illegal line count -- -5

# Workaround for BSD
$ tail -r file.txt | tail -n +6 | tail -r

tar: Option Syntax

# Extract archive (both, but syntax preference differs)
$ tar -xvf archive.tar.gz     # Works on both
$ tar xvf archive.tar.gz      # Also works on both

# Create archive (compatible)
$ tar -cvf archive.tar directory/

# Compression selection (mostly compatible)
$ tar -czvf archive.tar.gz directory/    # gzip
$ tar -cjvf archive.tar.bz2 directory/   # bzip2

# Auto-detect compression on extract
$ tar -xf archive.tar.gz    # Both auto-detect

# GNU tar has more compression options
$ tar --zstd -cvf archive.tar.zst directory/  # GNU only

stat: Completely Different

The stat command is almost entirely incompatible:

# File size (GNU)
$ stat -c %s file.txt
1024

# File size (BSD/macOS)
$ stat -f %z file.txt
1024

# Modification time (GNU)
$ stat -c %Y file.txt
1705276800

# Modification time (BSD)
$ stat -f %m file.txt
1705276800

# Human-readable (GNU)
$ stat file.txt
  File: file.txt
  Size: 1024            Blocks: 8          IO Block: 4096   regular file
...

# Human-readable (BSD)
$ stat file.txt
16777220 12345678 -rw-r--r-- 1 user staff 0 1024 "Jan 15 10:00:00 2024" ...

Portable alternative:

# Get file size portably
$ wc -c < file.txt
1024

# Or using ls
$ ls -l file.txt | awk '{print $5}'
1024
# Canonical path (GNU)
$ readlink -f /usr/local/bin/python
/opt/homebrew/Cellar/python@3.11/3.11.5/bin/python3.11

# BSD readlink doesn't have -f
$ readlink -f symlink
readlink: illegal option -- f

# BSD alternative
$ realpath symlink    # If available (macOS 12.3+)
$ python -c "import os; print(os.path.realpath('symlink'))"

# Or use this function
canonicalize() {
    cd -P "$(dirname "$1")" && echo "$(pwd)/$(basename "$1")"
}

mktemp: Temporary Files

# Create temp file (GNU)
$ mktemp
/tmp/tmp.Xa3B2c1D

# BSD mktemp requires template
$ mktemp
/var/folders/.../tmp.XXXXXXXX    # Actually works on modern macOS

# But explicit template is more portable
$ mktemp /tmp/myapp.XXXXXX
/tmp/myapp.a1b2c3

# Create temp directory (both support -d)
$ mktemp -d

Quick Reference Table

CommandGNU (Linux)BSD (macOS)
In-place sedsed -i 's/a/b/'sed -i '' 's/a/b/'
Perl regex grepgrep -P '\d+'Not available
Colored lsls --colorls -G
Date parsingdate -d "string"date -j -f "fmt" "str"
Relative datedate -d "+7 days"date -v+7d
File statsstat -c %s filestat -f %z file
Version sortsort -VNot available
Human size sortsort -hNot available
Canonical pathreadlink -frealpath (macOS 12.3+)
xargs parallelxargs -P 4Not available

Recommendations

  1. For scripts: Use POSIX-compatible syntax when possible
  2. For interactive use: Install GNU coreutils via Homebrew
  3. For maximum portability: Test on both platforms
  4. For macOS-only: BSD tools are fine, just learn their syntax

The next chapter covers macOS-specific commands that have no GNU equivalent, followed by how to install GNU tools alongside BSD tools.

Essential macOS-Specific Commands

macOS includes many commands that don’t exist on Linux—tools designed for Apple’s ecosystem. These commands interact with macOS services, Spotlight search, the clipboard, and system configuration. Mastering them makes you significantly more productive on macOS.

Clipboard: pbcopy and pbpaste

The pasteboard (clipboard) commands let you pipe data to and from the system clipboard:

# Copy command output to clipboard
$ ls -la | pbcopy

# Copy file contents to clipboard
$ pbcopy < ~/.ssh/id_rsa.pub

# Paste clipboard contents
$ pbpaste

# Paste to a file
$ pbpaste > clipped.txt

# Use clipboard contents in a command
$ pbpaste | wc -l

# Transform clipboard contents
$ pbpaste | sort | uniq | pbcopy

Real-world examples:

# Copy current directory path
$ pwd | pbcopy

# Copy a file's contents with line numbers
$ cat -n script.sh | pbcopy

# Copy output of a complex command
$ git diff HEAD~5 | pbcopy

# Convert clipboard to uppercase
$ pbpaste | tr '[:lower:]' '[:upper:]' | pbcopy

# Copy just filenames from ls output
$ ls | pbcopy

# Format JSON from clipboard
$ pbpaste | python -m json.tool | pbcopy

Linux equivalents require additional tools:

# Linux clipboard (X11)
$ command | xclip -selection clipboard
$ xclip -selection clipboard -o

# Linux clipboard (Wayland)
$ command | wl-copy
$ wl-paste

open: Launch Files and Applications

The open command launches files with their default application:

# Open file with default application
$ open document.pdf
$ open image.png
$ open movie.mp4

# Open current directory in Finder
$ open .

# Open URL in default browser
$ open https://apple.com

# Open specific application
$ open -a "Visual Studio Code" myfile.txt
$ open -a Safari https://github.com

# Open with specific application (by bundle ID)
$ open -b com.apple.TextEdit file.txt

# Reveal file in Finder (don't open, just show)
$ open -R myfile.txt

# Open new instance of an application
$ open -n -a "Google Chrome"

# Open and wait until app closes
$ open -W document.pdf    # Script pauses until you close Preview

# Open multiple files
$ open *.txt

# Open in background (don't bring to front)
$ open -g file.pdf

Open with text editors:

# Open in TextEdit
$ open -a TextEdit notes.txt
$ open -e notes.txt    # Shorthand for TextEdit

# Open in VS Code
$ open -a "Visual Studio Code" .
$ code .    # If VS Code shell command is installed

Linux equivalent:

# Linux
$ xdg-open document.pdf
$ xdg-open https://example.com

mdfind: Spotlight Search from Terminal

mdfind queries the Spotlight index—much faster than find for searching file contents:

# Search by filename
$ mdfind -name "report.pdf"

# Search by content
$ mdfind "project deadline"

# Search in specific directory
$ mdfind -onlyin ~/Documents "meeting notes"

# Combine filename and content search
$ mdfind -name ".py" "import pandas"

# Search by file type
$ mdfind "kMDItemContentType == 'public.plain-text'"

# Find files modified today
$ mdfind 'kMDItemFSContentChangeDate >= $time.today'

# Find files by author
$ mdfind "kMDItemAuthors == 'John Smith'"

# Find images
$ mdfind "kMDItemContentTypeTree == 'public.image'"

# Find applications
$ mdfind "kMDItemKind == 'Application'"

Complex queries:

# Large files modified this week
$ mdfind 'kMDItemFSSize > 100000000 && kMDItemFSContentChangeDate > $time.this_week'

# PDFs containing "invoice" in Downloads
$ mdfind -onlyin ~/Downloads "kMDItemContentType == 'com.adobe.pdf' && invoice"

# Find presentations
$ mdfind "kMDItemKind == 'Keynote Document' || kMDItemKind == 'PowerPoint Presentation'"

# Count results
$ mdfind -count "kMDItemContentType == 'public.python-script'"
42

Useful aliases:

# Add to ~/.zshrc
alias findf='mdfind -name'
alias findtext='mdfind -onlyin .'

mdls: View Spotlight Metadata

mdls shows all metadata Spotlight knows about a file:

$ mdls document.pdf
kMDItemAuthors                     = (
    "John Smith"
)
kMDItemContentCreationDate         = 2024-01-15 10:00:00 +0000
kMDItemContentModificationDate     = 2024-01-15 14:30:00 +0000
kMDItemContentType                 = "com.adobe.pdf"
kMDItemContentTypeTree             = (
    "com.adobe.pdf",
    "public.data",
    "public.item"
)
kMDItemDisplayName                 = "document.pdf"
kMDItemFSContentChangeDate         = 2024-01-15 14:30:00 +0000
kMDItemFSCreationDate              = 2024-01-15 10:00:00 +0000
kMDItemFSName                      = "document.pdf"
kMDItemFSSize                      = 1048576
kMDItemKind                        = "PDF Document"
kMDItemNumberOfPages               = 10
kMDItemPageHeight                  = 792
kMDItemPageWidth                   = 612
...

Query specific attributes:

# Get just file size
$ mdls -name kMDItemFSSize document.pdf
kMDItemFSSize = 1048576

# Get multiple attributes
$ mdls -name kMDItemContentType -name kMDItemFSSize file.txt

# Raw value (no attribute name)
$ mdls -raw -name kMDItemFSSize document.pdf
1048576

# List all photos with dimensions
$ for f in *.jpg; do
    echo "$f: $(mdls -raw -name kMDItemPixelWidth "$f")x$(mdls -raw -name kMDItemPixelHeight "$f")"
done

say: Text-to-Speech

The say command converts text to speech:

# Basic usage
$ say "Hello, world"

# Read from stdin
$ echo "The process is complete" | say

# Read a file
$ say -f script.txt

# Different voices
$ say -v Alex "Hello"
$ say -v Samantha "Hello"
$ say -v Daniel "Hello"    # British English

# List available voices
$ say -v '?'
Alex                en_US    # Most people recognize me by my voice.
Alice               it_IT    # Salve, mi chiamo Alice e sono una voce italiana.
Alva                sv_SE    # Hej, jag heter Alva. Jag är en svensk röst.
...

# Save to audio file
$ say -o greeting.aiff "Welcome to the application"

# Save as different format
$ say -o greeting.m4a --data-format=aac "Hello"

# Adjust rate (words per minute, default ~175)
$ say -r 100 "Slow speech"
$ say -r 250 "Fast speech"

Practical uses:

# Notification when long command completes
$ make all && say "Build complete" || say "Build failed"

# Read documentation aloud
$ man ls | col -b | say

# Countdown timer
$ for i in 5 4 3 2 1; do say $i; sleep 1; done; say "Time is up"

screencapture: Screenshots from Terminal

Take screenshots without keyboard shortcuts:

# Capture entire screen
$ screencapture screenshot.png

# Capture specific window (interactive selection)
$ screencapture -W screenshot.png

# Capture selection (interactive)
$ screencapture -s screenshot.png

# Capture to clipboard instead of file
$ screencapture -c

# Capture after delay (seconds)
$ screencapture -T 5 screenshot.png

# Capture window without shadow
$ screencapture -o -W screenshot.png

# Capture specific display (multi-monitor)
$ screencapture -D 1 screenshot.png

# Capture in different formats
$ screencapture -t pdf screenshot.pdf
$ screencapture -t jpg screenshot.jpg

# Capture and open in Preview
$ screencapture -P screenshot.png

# Silent capture (no camera sound)
$ screencapture -x screenshot.png

# Capture touch bar (if available)
$ screencapture -b touchbar.png

Automate screenshots:

# Timed screenshot sequence
$ for i in {1..10}; do
    screencapture -x "screen_$i.png"
    sleep 60
done

# Screenshot with timestamp
$ screencapture "screenshot_$(date +%Y%m%d_%H%M%S).png"

networksetup: Network Configuration

Configure network settings from the terminal:

# List all network services
$ networksetup -listallnetworkservices
An asterisk (*) denotes that a network service is disabled.
Wi-Fi
Bluetooth PAN
Thunderbolt Bridge
iPhone USB

# List hardware ports
$ networksetup -listallhardwareports
Hardware Port: Wi-Fi
Device: en0
Ethernet Address: aa:bb:cc:dd:ee:ff

Hardware Port: Thunderbolt 1
Device: en1
...

# Get current Wi-Fi network
$ networksetup -getairportnetwork en0
Current Wi-Fi Network: MyNetwork

# Get IP address for interface
$ networksetup -getinfo Wi-Fi
DHCP Configuration
IP address: 192.168.1.100
Subnet mask: 255.255.255.0
Router: 192.168.1.1
Client ID:
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Wi-Fi ID: aa:bb:cc:dd:ee:ff

# Set to DHCP
$ sudo networksetup -setdhcp Wi-Fi

# Set static IP
$ sudo networksetup -setmanual Wi-Fi 192.168.1.50 255.255.255.0 192.168.1.1

# Set DNS servers
$ sudo networksetup -setdnsservers Wi-Fi 8.8.8.8 8.8.4.4

# Get DNS servers
$ networksetup -getdnsservers Wi-Fi
8.8.8.8
8.8.4.4

# Clear custom DNS (use DHCP-provided)
$ sudo networksetup -setdnsservers Wi-Fi empty

# Turn Wi-Fi on/off
$ networksetup -setairportpower en0 on
$ networksetup -setairportpower en0 off

# Get Wi-Fi power state
$ networksetup -getairportpower en0
Wi-Fi Power (en0): On

# Join a Wi-Fi network
$ networksetup -setairportnetwork en0 "NetworkName" "password"

# Set proxy
$ sudo networksetup -setwebproxy Wi-Fi proxy.example.com 8080

# Get proxy settings
$ networksetup -getwebproxy Wi-Fi

# Disable proxy
$ sudo networksetup -setwebproxystate Wi-Fi off

# Set network service order (priority)
$ sudo networksetup -ordernetworkservices "Wi-Fi" "Ethernet" "Bluetooth PAN"

scutil: System Configuration Utility

Query and modify system configuration:

# Get hostname
$ scutil --get HostName
my-mac

$ scutil --get LocalHostName
my-mac

$ scutil --get ComputerName
My Mac

# Set hostname (requires sudo)
$ sudo scutil --set HostName newname
$ sudo scutil --set LocalHostName newname
$ sudo scutil --set ComputerName "New Name"

# Check network reachability
$ scutil -r apple.com
Reachable,Direct

# Interactive mode (explore system configuration)
$ scutil
> list
  subKey [0] = Plugin:IPConfiguration
  subKey [1] = Plugin:InterfaceNamer
  subKey [2] = Setup:
  ...
> show State:/Network/Global/IPv4
<dictionary> {
  PrimaryInterface : en0
  PrimaryService : XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
  Router : 192.168.1.1
}
> quit

# Get DNS configuration
$ scutil --dns
DNS configuration

resolver #1
  nameserver[0] : 192.168.1.1
  if_index : 8 (en0)
  ...

# Get proxy configuration
$ scutil --proxy
<dictionary> {
  ExceptionsList : <array> {
    0 : *.local
    1 : 169.254/16
  }
  FTPPassive : 1
  HTTPEnable : 0
  ...
}

defaults: Read and Write System Preferences

The defaults command reads and writes macOS preference files (.plist):

# Read an application's preferences
$ defaults read com.apple.finder

# Read specific key
$ defaults read com.apple.finder ShowExternalHardDrivesOnDesktop
1

# Write a preference
$ defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool false

# Delete a preference (revert to default)
$ defaults delete com.apple.finder ShowExternalHardDrivesOnDesktop

# Read global preferences
$ defaults read NSGlobalDomain

# Show hidden files in Finder
$ defaults write com.apple.finder AppleShowAllFiles -bool true
$ killall Finder    # Restart Finder to apply

# Disable auto-correct
$ defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false

# Set default screenshot location
$ defaults write com.apple.screencapture location ~/Screenshots
$ killall SystemUIServer

# Set default screenshot format
$ defaults write com.apple.screencapture type png    # png, jpg, gif, pdf

# Speed up dock animations
$ defaults write com.apple.dock autohide-time-modifier -float 0.15
$ killall Dock

# List all domains
$ defaults domains

Common customizations:

# Show full path in Finder title
$ defaults write com.apple.finder _FXShowPosixPathInTitle -bool true

# Disable .DS_Store on network volumes
$ defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true

# Enable text selection in Quick Look
$ defaults write com.apple.finder QLEnableTextSelection -bool true

# Expand save panel by default
$ defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode -bool true

# Show all filename extensions
$ defaults write NSGlobalDomain AppleShowAllExtensions -bool true

# Disable the warning when changing file extension
$ defaults write com.apple.finder FXEnableExtensionChangeWarning -bool false

airport: Wi-Fi Diagnostics

The airport command is hidden but powerful:

# Create alias (the path is inconvenient)
$ alias airport='/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'

# Show current connection info
$ airport -I
     agrCtlRSSI: -52
     agrExtRSSI: 0
    agrCtlNoise: -88
    agrExtNoise: 0
          state: running
        op mode: station
     lastTxRate: 1200
        maxRate: 1200
lastAssocStatus: 0
    802.11 auth: open
      link auth: wpa2-psk
          BSSID: aa:bb:cc:dd:ee:ff
           SSID: MyNetwork
            MCS: 11
  guardInterval: 800
            NSS: 2
        channel: 149,80

# Scan for available networks
$ airport -s
                            SSID BSSID             RSSI CHANNEL HT CC SECURITY
                       MyNetwork aa:bb:cc:dd:ee:ff -52  149,+1  Y  US WPA2(PSK/AES)
                    Neighbor_5GHz bb:cc:dd:ee:ff:00 -65  36      Y  US WPA2(PSK/AES)
                  Office-Guest cc:dd:ee:ff:00:11 -70  6       Y  US WPA2(PSK/AES)

# Disconnect from current network
$ sudo airport -z

Other Useful macOS Commands

caffeinate: Prevent Sleep

# Prevent sleep while command runs
$ caffeinate -i long_running_process

# Prevent sleep for specified time (seconds)
$ caffeinate -t 3600    # 1 hour

# Prevent display sleep
$ caffeinate -d

# Prevent disk sleep
$ caffeinate -m

# Create assertion until process exits
$ caffeinate -w PID

diskutil: Disk Management

# List disks
$ diskutil list

# Get info about a disk
$ diskutil info disk0

# Eject a disk
$ diskutil eject disk2

# Mount/unmount volumes
$ diskutil mount disk2s1
$ diskutil unmount disk2s1

# Format a disk (CAREFUL!)
$ diskutil eraseDisk APFS NewDisk disk2

pmset: Power Management

# Show current power settings
$ pmset -g

# Show battery info
$ pmset -g batt
Now drawing from 'Battery Power'
 -InternalBattery-0 (id=123456)	78%; discharging; 3:45 remaining

# Show sleep settings
$ pmset -g custom

# Prevent sleep (requires sudo)
$ sudo pmset -a disablesleep 1

# Schedule shutdown
$ sudo pmset schedule shutdown "01/20/2024 23:00:00"

textutil: Document Conversion

# Convert RTF to plain text
$ textutil -convert txt document.rtf

# Convert Word doc to HTML
$ textutil -convert html document.docx

# Convert multiple files
$ textutil -convert txt *.rtf

# Get document info
$ textutil -info document.docx

sips: Image Processing

# Get image dimensions
$ sips -g pixelWidth -g pixelHeight image.jpg

# Resize image
$ sips -Z 800 image.jpg    # Max dimension 800, maintain aspect ratio

# Convert format
$ sips -s format png image.jpg --out image.png

# Rotate image
$ sips -r 90 image.jpg

# Batch resize
$ sips -Z 1200 *.jpg

Summary

macOS-specific commands fill gaps that Linux handles differently:

macOS CommandPurposeLinux Equivalent
pbcopy/pbpasteClipboard accessxclip, xsel
openLaunch files/appsxdg-open
mdfindSearch file contentslocate, grep -r
mdlsView file metadatastat, file
sayText-to-speechespeak, festival
screencaptureScreenshotsgnome-screenshot, scrot
networksetupNetwork confignmcli, ip
scutilSystem configVarious tools
defaultsPreferencesgsettings, dconf
caffeinatePrevent sleepsystemd-inhibit

These commands are part of what makes macOS unique. Learn them, and you’ll work more efficiently than users who rely solely on GUI tools.

Installing and Using GNU Coreutils

If you’re frustrated by BSD tool limitations or need scripts to behave the same way on macOS and Linux, you can install GNU coreutils. This gives you the familiar GNU versions while keeping BSD tools available as a fallback.

Installation via Homebrew

Install individual GNU tools or the complete coreutils package:

# Install GNU coreutils (includes ls, cp, mv, rm, date, etc.)
$ brew install coreutils

# Install individual GNU tools
$ brew install gnu-sed    # GNU sed
$ brew install grep       # GNU grep (note: Homebrew grep IS GNU grep)
$ brew install gawk       # GNU awk
$ brew install findutils  # GNU find, xargs, locate
$ brew install gnu-tar    # GNU tar
$ brew install gnu-which  # GNU which

# Install everything you might need
$ brew install coreutils gnu-sed gawk findutils gnu-tar grep

Understanding the g-prefix

To avoid breaking system scripts that depend on BSD tools, Homebrew installs GNU commands with a g prefix:

# BSD tools (system default)
$ which ls
/bin/ls

$ which sed
/usr/bin/sed

# GNU tools (g-prefixed)
$ which gls
/opt/homebrew/bin/gls

$ which gsed
/opt/homebrew/bin/gsed

Use GNU tools by their g-prefixed names:

# GNU versions
$ gls --color=auto
$ gsed -i 's/old/new/' file.txt
$ ggrep -P '\d+' file.txt
$ gawk 'BEGIN {print "hello"}'
$ gfind . -name "*.txt"
$ gdate -d "yesterday"
$ gtar --zstd -cvf archive.tar.zst directory/

Making GNU Tools the Default

You can add GNU tool paths to your PATH so they’re used instead of BSD tools. Homebrew’s coreutils installs unprefixed versions in a specific directory:

# Check where unprefixed versions are installed
$ ls /opt/homebrew/opt/coreutils/libexec/gnubin/
base32  cat     chgrp   chmod   chown   chroot  cksum   comm    cp
csplit  cut     date    dd      df      dir     dircolors  dirname
du      echo    env     expand  expr    factor  false   fmt     fold
...

# Same for other GNU packages
$ ls /opt/homebrew/opt/gnu-sed/libexec/gnubin/
sed

$ ls /opt/homebrew/opt/findutils/libexec/gnubin/
find  locate  updatedb  xargs

Add to your shell configuration (~/.zshrc or ~/.bash_profile):

# Add GNU coreutils to PATH
if [ -d "/opt/homebrew/opt/coreutils/libexec/gnubin" ]; then
    export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"
fi

# Add GNU sed
if [ -d "/opt/homebrew/opt/gnu-sed/libexec/gnubin" ]; then
    export PATH="/opt/homebrew/opt/gnu-sed/libexec/gnubin:$PATH"
fi

# Add GNU findutils
if [ -d "/opt/homebrew/opt/findutils/libexec/gnubin" ]; then
    export PATH="/opt/homebrew/opt/findutils/libexec/gnubin:$PATH"
fi

# Add GNU tar
if [ -d "/opt/homebrew/opt/gnu-tar/libexec/gnubin" ]; then
    export PATH="/opt/homebrew/opt/gnu-tar/libexec/gnubin:$PATH"
fi

# Add grep (Homebrew's grep is already GNU grep, no gnubin needed)
if [ -d "/opt/homebrew/opt/grep/libexec/gnubin" ]; then
    export PATH="/opt/homebrew/opt/grep/libexec/gnubin:$PATH"
fi

# Add man pages for GNU tools
if [ -d "/opt/homebrew/opt/coreutils/libexec/gnuman" ]; then
    export MANPATH="/opt/homebrew/opt/coreutils/libexec/gnuman:$MANPATH"
fi

Reload your shell:

$ source ~/.zshrc

# Verify GNU tools are now default
$ which ls
/opt/homebrew/opt/coreutils/libexec/gnubin/ls

$ ls --version
ls (GNU coreutils) 9.4

One-Line PATH Setup

For a comprehensive setup, add this to your shell configuration:

# GNU tools PATH setup
for gnubin in /opt/homebrew/opt/*/libexec/gnubin; do
    export PATH="$gnubin:$PATH"
done

This automatically adds all installed GNU tools to your PATH.

Maintaining Access to BSD Tools

Even with GNU tools in PATH, you can still access BSD tools:

# Explicit path to BSD tools
$ /bin/ls
$ /usr/bin/sed
$ /usr/bin/grep
$ /usr/bin/awk
$ /usr/bin/find

# Create aliases for easy access
alias bls='/bin/ls'
alias bsed='/usr/bin/sed'
alias bgrep='/usr/bin/grep'
alias bawk='/usr/bin/awk'

Selective GNU Tool Usage

You might want only specific GNU tools as defaults. Here’s a targeted approach:

# ~/.zshrc - Only add specific GNU tools

# GNU sed (most commonly needed)
alias sed='gsed'

# GNU grep with Perl regex
alias grep='ggrep'

# GNU awk
alias awk='gawk'

# GNU date
alias date='gdate'

# Keep ls as BSD (some prefer its output)
# alias ls='gls --color=auto'

Or use a function that tries GNU first, falls back to BSD:

# Try GNU version, fall back to BSD
sed() {
    if command -v gsed &> /dev/null; then
        gsed "$@"
    else
        /usr/bin/sed "$@"
    fi
}

Verifying Your Setup

Check which versions are active:

# Create a test script
$ cat << 'EOF' > check_tools.sh
#!/bin/bash
echo "=== Tool Versions ==="
echo -n "ls:   "; ls --version 2>&1 | head -1 || echo "BSD (no --version)"
echo -n "sed:  "; sed --version 2>&1 | head -1 || echo "BSD (no --version)"
echo -n "grep: "; grep --version 2>&1 | head -1 || echo "BSD (no --version)"
echo -n "awk:  "; awk --version 2>&1 | head -1 || echo "BSD (no --version)"
echo -n "find: "; find --version 2>&1 | head -1 || echo "BSD (no --version)"
echo -n "date: "; date --version 2>&1 | head -1 || echo "BSD (no --version)"
echo -n "tar:  "; tar --version 2>&1 | head -1 || echo "BSD (no --version)"

echo ""
echo "=== Tool Paths ==="
which ls sed grep awk find date tar
EOF

$ chmod +x check_tools.sh
$ ./check_tools.sh

Using Both Versions in Scripts

Sometimes you need to use both versions. Here’s how to be explicit:

#!/bin/bash
# Script that uses both BSD and GNU tools

# Define tool paths explicitly
GNU_SED="/opt/homebrew/opt/gnu-sed/libexec/gnubin/sed"
BSD_SED="/usr/bin/sed"

# Use GNU sed for complex regex
$GNU_SED -i 's/\bword\b/replacement/g' file.txt

# Use BSD sed if script needs to be portable
# (with BSD syntax)
$BSD_SED -i '' 's/simple/replace/' file.txt

Or detect and adapt:

#!/bin/bash

# Detect sed type and set options accordingly
if sed --version 2>&1 | grep -q GNU; then
    SED_INPLACE="-i"
else
    SED_INPLACE="-i ''"
fi

# Now use: eval sed $SED_INPLACE 's/old/new/' file.txt

Potential Issues

Script Compatibility

Some macOS system scripts expect BSD behavior. If you make GNU tools the default:

# Potential issue: macOS scripts expecting BSD tools
# Solution: Use full paths in system scripts or
# don't add GNU tools to global PATH for root

Homebrew’s grep vs System grep

Note that brew install grep installs GNU grep:

$ brew install grep
$ /opt/homebrew/bin/grep --version
grep (GNU grep) 3.11

# The g-prefixed version
$ /opt/homebrew/bin/ggrep --version
grep (GNU grep) 3.11

# They're the same
$ ls -la /opt/homebrew/bin/ggrep
lrwxr-xr-x  1 user  staff  28 Jan 15 10:00 /opt/homebrew/bin/ggrep -> ../Cellar/grep/3.11/bin/ggrep

Intel vs Apple Silicon Paths

Paths differ by architecture:

# Apple Silicon
/opt/homebrew/opt/coreutils/libexec/gnubin

# Intel
/usr/local/opt/coreutils/libexec/gnubin

# Portable approach in shell config
HOMEBREW_PREFIX=$(brew --prefix 2>/dev/null || echo "/opt/homebrew")
export PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH"

Complete Setup Example

Here’s a complete ~/.zshrc snippet for a Linux-like experience:

# ~/.zshrc - GNU tools configuration

# Homebrew prefix (works on both Intel and Apple Silicon)
HOMEBREW_PREFIX="${HOMEBREW_PREFIX:-$(brew --prefix 2>/dev/null)}"

if [ -n "$HOMEBREW_PREFIX" ]; then
    # Add GNU tools to PATH (order matters - first takes precedence)
    for pkg in coreutils gnu-sed gnu-tar findutils grep; do
        gnubin="$HOMEBREW_PREFIX/opt/$pkg/libexec/gnubin"
        if [ -d "$gnubin" ]; then
            export PATH="$gnubin:$PATH"
        fi
    done

    # Add GNU man pages
    for pkg in coreutils gnu-sed gnu-tar findutils grep; do
        gnuman="$HOMEBREW_PREFIX/opt/$pkg/libexec/gnuman"
        if [ -d "$gnuman" ]; then
            export MANPATH="$gnuman:${MANPATH:-}"
        fi
    done
fi

# Aliases for GNU tools that don't use gnubin
alias awk='gawk'

# Enable colors for GNU ls
alias ls='ls --color=auto'
alias ll='ls -la --color=auto'

# Keep BSD tools accessible
alias bsd-ls='/bin/ls'
alias bsd-sed='/usr/bin/sed'
alias bsd-grep='/usr/bin/grep'

Troubleshooting

“illegal option” Errors

You’re using GNU syntax with BSD tools:

$ ls --color
ls: illegal option -- -
# Solution: Check which ls is in PATH
$ which ls
/bin/ls    # BSD version
# Either use: gls --color
# Or add GNU coreutils to PATH

Man Pages Show Wrong Version

$ man ls    # Shows BSD man page even though GNU ls is used
# Add GNU man paths
export MANPATH="/opt/homebrew/opt/coreutils/libexec/gnuman:$MANPATH"

Homebrew Commands Not Found

$ gsed
zsh: command not found: gsed
# GNU sed not installed
$ brew install gnu-sed

Summary

GNU PackageCommands Includedg-prefix Examples
coreutilsls, cp, mv, rm, date, cat, chmod, etc.gls, gcp, gmv, grm, gdate
gnu-sedsedgsed
grepgrep, egrep, fgrepggrep
gawkawkgawk
findutilsfind, xargs, locategfind, gxargs, glocate
gnu-tartargtar
gnu-whichwhichgwhich

Installing GNU tools gives you:

  • Familiar Linux behavior on macOS
  • Perl-compatible regex in grep (-P)
  • Easier in-place editing with sed
  • Version sorting in sort (-V)
  • Human-readable sort (-h)
  • And many more GNU extensions

The trade-off is managing PATH carefully and being aware of which version you’re using, especially when writing scripts meant for others.

Text Processing: sed, awk, and grep

Text processing is where BSD and GNU tools diverge most significantly. Scripts that work perfectly on Linux often fail on macOS due to subtle differences in sed, awk, and grep. This chapter provides equivalent commands for both platforms and explains when each syntax is required.

sed: Stream Editor

In-Place Editing

The most common gotcha when moving from Linux to macOS:

# GNU sed (Linux)
$ sed -i 's/old/new/g' file.txt

# BSD sed (macOS) - requires backup extension argument
$ sed -i '' 's/old/new/g' file.txt    # No backup created
$ sed -i '.bak' 's/old/new/g' file.txt  # Creates file.txt.bak

# The error you'll see using GNU syntax on BSD:
$ sed -i 's/old/new/g' file.txt
sed: 1: "file.txt": invalid command code f
# BSD interpreted 's/old/new/g' as the backup extension!

Newlines and Special Characters

# Insert newline - GNU sed
$ echo "hello" | sed 's/$/\n/'
hello
                    # (blank line)

# Insert newline - BSD sed (\n doesn't work)
# Method 1: Use $'...' quoting
$ echo "hello" | sed $'s/$/\\\n/'

# Method 2: Literal newline in the command
$ echo "hello" | sed 's/$/\
/'

# Method 3: Use a variable
$ NL=$'\n'
$ echo "hello" | sed "s/$/$NL/"

Tab characters:

# GNU sed - \t works
$ echo "a b" | sed 's/ /\t/'
a	b

# BSD sed - \t doesn't work in replacement
$ echo "a b" | sed $'s/ /\t/'
a	b

# Or use a literal tab
$ echo "a b" | sed 's/ /	/'    # Actual tab character

Extended Regular Expressions

# Basic regex (both platforms)
$ echo "hello" | sed 's/l\+/L/'
heLlo

# Extended regex - GNU
$ echo "hello" | sed -E 's/l+/L/'
heLo

# Extended regex - BSD (same -E flag, compatible!)
$ echo "hello" | sed -E 's/l+/L/'
heLo

# Note: GNU also accepts -r for extended regex
$ echo "hello" | sed -r 's/l+/L/'    # GNU only

Case-Insensitive Matching

# GNU sed supports 'i' flag
$ echo "Hello HELLO hello" | sed 's/hello/hi/gi'
hi hi hi

# BSD sed doesn't support 'i' flag
$ echo "Hello HELLO hello" | sed 's/hello/hi/gi'
sed: 1: "s/hello/hi/gi": bad flag in substitute command: 'i'

# BSD workaround: use character classes
$ echo "Hello HELLO hello" | sed 's/[Hh][Ee][Ll][Ll][Oo]/hi/g'
hi hi hi

# Or use tr for simple case conversion, then sed
$ echo "Hello HELLO hello" | tr '[:upper:]' '[:lower:]' | sed 's/hello/hi/g'

Multiple Commands

# Both support -e for multiple expressions
$ sed -e 's/a/A/' -e 's/b/B/' file.txt

# Both support semicolons
$ sed 's/a/A/; s/b/B/' file.txt

# Both support newlines in the script
$ sed '
s/a/A/
s/b/B/
' file.txt

Address Ranges

# Line numbers (compatible)
$ sed '10,20d' file.txt        # Delete lines 10-20
$ sed '5q' file.txt            # Print first 5 lines and quit

# Pattern addresses (compatible)
$ sed '/start/,/end/d' file.txt

# Last line (compatible)
$ sed '$d' file.txt            # Delete last line

# First match only - GNU sed
$ sed '0,/pattern/s/pattern/replace/' file.txt

# First match only - BSD sed (0 address not supported)
$ sed '1,/pattern/s/pattern/replace/' file.txt
# Note: BSD behavior differs if pattern is on line 1

Portable sed Script

#!/bin/bash
# Works on both BSD and GNU sed

# Detect sed type
if sed --version 2>&1 | grep -q GNU; then
    SED="sed"
    SED_I="sed -i"
else
    SED="sed"
    SED_I="sed -i ''"
fi

# Use with eval for in-place editing
eval $SED_I "'s/old/new/g'" file.txt

# Or use a temp file (most portable)
sed 's/old/new/g' file.txt > file.tmp && mv file.tmp file.txt

awk: Pattern Processing

awk on macOS is actually nawk (new awk), which is largely compatible with GNU awk. However, differences exist.

Basic Usage (Compatible)

# Print columns (compatible)
$ echo "a b c" | awk '{print $2}'
b

# Field separator (compatible)
$ echo "a:b:c" | awk -F: '{print $2}'
b

# Patterns (compatible)
$ awk '/error/ {print}' log.txt

# Variables (compatible)
$ awk -v name="John" 'BEGIN {print "Hello", name}'
Hello John

GNU awk Extensions

# Case-insensitive matching - GNU awk (gawk)
$ echo -e "Hello\nhello\nHELLO" | gawk 'BEGIN{IGNORECASE=1} /hello/'
Hello
hello
HELLO

# BSD awk doesn't have IGNORECASE
$ echo -e "Hello\nhello\nHELLO" | awk '/[Hh][Ee][Ll][Ll][Oo]/'

# Length function as array length - gawk
$ gawk 'BEGIN {a[1]=1; a[2]=2; print length(a)}'
2

# BSD awk - length(array) may not work
# Use a loop to count
$ awk 'BEGIN {a[1]=1; a[2]=2; for(i in a) n++; print n}'
2

Regular Expression Differences

# Word boundaries - GNU awk
$ echo "foo foobar" | gawk '{gsub(/\bfoo\b/, "bar"); print}'
bar foobar

# BSD awk doesn't support \b
$ echo "foo foobar" | awk '{gsub(/foo[^a-z]|foo$/, "bar "); print}'
bar foobar

# Interval expressions {n,m} - both support with --posix or -r
$ echo "aaa" | gawk '{print gsub(/a{2,3}/, "X")}'
1

$ echo "aaa" | awk '{print gsub(/a{2,3}/, "X")}'
# May or may not work depending on macOS version

In-Place Editing with awk

# GNU awk 4.1+ has -i inplace
$ gawk -i inplace '{gsub(/old/, "new")}1' file.txt

# BSD awk has no in-place option
# Use a temp file
$ awk '{gsub(/old/, "new")}1' file.txt > tmp && mv tmp file.txt

For consistent behavior, install GNU awk:

$ brew install gawk

# Use as gawk or add to PATH
$ gawk --version
GNU Awk 5.2.2, API 3.2, PMA Avon 8-g1

grep: Pattern Matching

Basic Usage (Compatible)

# Simple pattern (compatible)
$ grep "error" log.txt

# Case insensitive (compatible)
$ grep -i "error" log.txt

# Line numbers (compatible)
$ grep -n "error" log.txt

# Count matches (compatible)
$ grep -c "error" log.txt

# Invert match (compatible)
$ grep -v "debug" log.txt

# Recursive search (compatible)
$ grep -r "TODO" src/

# Only filenames (compatible)
$ grep -l "error" *.log

Extended Regular Expressions

# Extended regex (compatible with -E)
$ grep -E "error|warning" log.txt
$ grep -E "[0-9]{3}-[0-9]{4}" contacts.txt

# Equivalent using egrep (both platforms)
$ egrep "error|warning" log.txt

Perl-Compatible Regex

The biggest difference - GNU grep’s -P flag:

# GNU grep - Perl regex
$ grep -P '\d{3}-\d{4}' contacts.txt
$ grep -P '(?<=@)\w+(?=\.com)' emails.txt    # Lookbehind/ahead

# BSD grep - no -P flag
$ grep -P '\d+'
grep: invalid option -- P

# Alternatives on BSD:

# 1. Use extended regex equivalents
$ grep -E '[0-9]{3}-[0-9]{4}' contacts.txt

# 2. Use perl directly
$ perl -ne 'print if /\d{3}-\d{4}/' contacts.txt

# 3. Install GNU grep
$ brew install grep
$ ggrep -P '\d{3}-\d{4}' contacts.txt

Common Perl Regex Features and BSD Alternatives

# Digits: \d (Perl) vs [0-9] (POSIX)
$ ggrep -P '\d+'          # GNU
$ grep -E '[0-9]+'        # BSD

# Word characters: \w vs [a-zA-Z0-9_]
$ ggrep -P '\w+'          # GNU
$ grep -E '[a-zA-Z0-9_]+' # BSD

# Word boundaries: \b vs [[:<:]] and [[:>:]]
$ ggrep -P '\bword\b'     # GNU
$ grep -E '[[:<:]]word[[:>:]]'  # BSD

# Non-greedy matching: .*?
$ ggrep -oP '<.*?>'       # GNU - minimal match
# BSD has no equivalent; use different approach
$ grep -oE '<[^>]*>'      # Matches <...> without > inside

# Lookahead/lookbehind
$ ggrep -P '(?<=\$)\d+'   # GNU - digits after $
# BSD has no equivalent

Context Lines

# Lines before match (compatible)
$ grep -B 3 "error" log.txt

# Lines after match (compatible)
$ grep -A 3 "error" log.txt

# Lines before and after (compatible)
$ grep -C 3 "error" log.txt

Line-Buffered Output

# Both support --line-buffered for live log watching
$ tail -f log.txt | grep --line-buffered "error"

sort: Sorting Text

Basic Sorting (Compatible)

# Alphabetical sort (compatible)
$ sort file.txt

# Reverse sort (compatible)
$ sort -r file.txt

# Numeric sort (compatible)
$ sort -n numbers.txt

# Sort by field (compatible)
$ sort -t: -k2 /etc/passwd

GNU-Only Features

# Version sort - GNU only
$ printf "1.10\n1.2\n1.1" | sort -V
1.1
1.2
1.10

$ printf "1.10\n1.2\n1.1" | sort -V    # BSD
sort: invalid option -- V

# BSD workaround (complex, not exact equivalent)
$ printf "1.10\n1.2\n1.1" | sort -t. -k1,1n -k2,2n
1.1
1.2
1.10

# Human-readable sizes - GNU only
$ du -h | sort -h
1K    small/
10M   medium/
2G    large/

$ du -h | sort -h    # BSD
sort: invalid option -- h

Stable Sort

# Both support stable sort
$ sort -s file.txt

# Random sort - both support
$ sort -R file.txt

cut: Extract Fields

Basic Usage (Compatible)

# Cut by character position (compatible)
$ echo "hello" | cut -c1-3
hel

# Cut by field (compatible)
$ echo "a:b:c" | cut -d: -f2
b

# Multiple fields (compatible)
$ echo "a:b:c" | cut -d: -f1,3
a:c

# Range of fields (compatible)
$ echo "a:b:c:d:e" | cut -d: -f2-4
b:c:d

GNU Extensions

# Complement - GNU only
$ echo "a:b:c" | cut -d: --complement -f2
a:c

$ echo "a:b:c" | cut -d: --complement -f2    # BSD
cut: illegal option -- -

# BSD alternative using awk
$ echo "a:b:c" | awk -F: '{print $1":"$3}'
a:c

# Output delimiter - GNU only
$ echo "a:b:c" | cut -d: -f1,3 --output-delimiter=' '
a c

# BSD alternative
$ echo "a:b:c" | cut -d: -f1,3 | tr ':' ' '
a c

tr: Translate Characters

tr is mostly compatible between BSD and GNU:

# Character substitution (compatible)
$ echo "hello" | tr 'a-z' 'A-Z'
HELLO

# Delete characters (compatible)
$ echo "hello 123" | tr -d '0-9'
hello

# Squeeze repeated characters (compatible)
$ echo "heeello" | tr -s 'e'
hello

# Character classes (compatible)
$ echo "Hello123" | tr -d '[:digit:]'
Hello

# Complement (compatible)
$ echo "hello 123" | tr -dc '0-9\n'
123

uniq: Filter Duplicates

Mostly compatible:

# Remove adjacent duplicates (compatible)
$ sort file.txt | uniq

# Count occurrences (compatible)
$ sort file.txt | uniq -c

# Only duplicates (compatible)
$ sort file.txt | uniq -d

# Only unique (compatible)
$ sort file.txt | uniq -u

# Case insensitive (compatible)
$ sort file.txt | uniq -i

Portable Text Processing Tips

1. Use temp files instead of in-place editing

# Most portable
sed 's/old/new/g' file.txt > file.tmp && mv file.tmp file.txt

2. Avoid GNU-specific regex

# Instead of \d, use [0-9]
# Instead of \w, use [a-zA-Z0-9_]
# Instead of \s, use [[:space:]]
# Instead of \b, use [[:<:]] and [[:>:]] (BSD) or word boundary logic

3. Create wrapper functions

# Add to ~/.zshrc or script
portable_sed_i() {
    if sed --version 2>&1 | grep -q GNU; then
        sed -i "$@"
    else
        sed -i '' "$@"
    fi
}

4. Use awk for complex transformations

awk is more consistent across platforms than sed for complex operations:

# Instead of complex sed
$ awk '{gsub(/old/, "new"); print}' file.txt > tmp && mv tmp file.txt

5. Test on both platforms

Before distributing scripts, test on both macOS and Linux:

# Check for GNU-specific features
$ shellcheck myscript.sh    # Static analysis

# Test in Docker for Linux
$ docker run --rm -v "$PWD:/work" -w /work alpine:latest sh myscript.sh

Summary: BSD vs GNU Text Tools

FeatureGNUBSDPortable Alternative
sed -ised -ised -i ''temp file
sed \nWorksDoesn’t work$‘\n’ or literal
grep -PWorksDoesn’t existUse -E with POSIX
sort -VWorksDoesn’t existCustom solution
sort -hWorksDoesn’t existSort raw bytes
cut –complementWorksDoesn’t existUse awk
awk IGNORECASEWorksDoesn’t existCharacter classes

When in doubt, install GNU tools via Homebrew and use them explicitly, or write POSIX-compliant commands that work everywhere.

File Operations and Manipulation

File operations on macOS have unique considerations: extended attributes, resource forks, ACLs, and macOS-specific tools like ditto. This chapter covers practical file operations while handling macOS-specific metadata correctly.

cp: Copying Files

Basic Operations (Mostly Compatible)

# Copy file (compatible)
$ cp source.txt dest.txt

# Copy directory recursively (compatible)
$ cp -R source_dir/ dest_dir/

# Preserve permissions and timestamps (compatible)
$ cp -p source.txt dest.txt

# Interactive mode - prompt before overwrite (compatible)
$ cp -i source.txt dest.txt

# Verbose output (compatible)
$ cp -v source.txt dest.txt

macOS-Specific Considerations

# Preserve extended attributes and resource forks
$ cp -p source.txt dest.txt    # -p includes xattrs on macOS

# Explicitly preserve all metadata
$ cp -a source_dir/ dest_dir/   # Same as -pPR (preserves everything)

# Don't follow symbolic links (compatible)
$ cp -P symlink dest/

# Copy without extended attributes (macOS specific)
# There's no direct flag; use ditto or strip after

Progress Indicator

# GNU cp has --progress (not available on macOS)
$ cp --progress large_file.iso /dest/    # Linux
cp: illegal option -- -                   # macOS

# macOS alternatives:

# 1. Use rsync with progress
$ rsync --progress large_file.iso /dest/

# 2. Use pv (pipe viewer, install via brew)
$ brew install pv
$ pv source.iso > dest/source.iso

# 3. Use macOS cp with Ctrl+T for status
$ cp large_file.iso /dest/
# Press Ctrl+T during copy to see progress
load: 2.45  cmd: cp 12345 uninterruptible 0.00u 1.23s
source.iso -> /dest/source.iso  45%

Handling Conflicts

# Don't overwrite existing (compatible)
$ cp -n source.txt dest.txt

# Force overwrite (compatible)
$ cp -f source.txt dest.txt

# Update only if source is newer - GNU only
$ cp -u source.txt dest.txt

# macOS alternative for update behavior
$ rsync -u source.txt dest.txt

mv: Moving and Renaming

Basic Operations (Compatible)

# Move file
$ mv source.txt /new/location/

# Rename file
$ mv oldname.txt newname.txt

# Move directory
$ mv source_dir/ /new/location/

# Interactive mode
$ mv -i source.txt dest.txt

# Don't overwrite existing
$ mv -n source.txt dest.txt

# Force overwrite
$ mv -f source.txt dest.txt

# Verbose
$ mv -v source.txt dest.txt

Update Mode (GNU only)

# GNU mv - only if source is newer
$ mv -u source.txt dest.txt

# macOS alternative
$ rsync -u --remove-source-files source.txt dest.txt

Moving Across Volumes

When moving files between volumes, mv copies then deletes (can’t just rename):

# Moving between volumes preserves metadata on macOS
$ mv file.txt /Volumes/ExternalDisk/

# For large moves, rsync gives progress
$ rsync -av --progress --remove-source-files source/ /Volumes/External/dest/

rm: Removing Files

Basic Operations (Compatible)

# Remove file
$ rm file.txt

# Remove directory and contents
$ rm -r directory/

# Force remove (no prompts)
$ rm -f file.txt

# Interactive mode
$ rm -i file.txt

# Verbose
$ rm -v file.txt

Secure Delete

# Secure delete - macOS (deprecated but still works)
$ rm -P file.txt    # 3-pass overwrite before delete

# Note: On SSDs with TRIM, secure delete is less effective
# FileVault encryption is more reliable for security

Safe Remove Practices

# Trash instead of delete (use trash-cli)
$ brew install trash-cli
$ trash file.txt    # Moves to Trash instead of deleting

# Or use AppleScript
$ osascript -e 'tell app "Finder" to delete POSIX file "'$(pwd)/file.txt'"'

ditto: macOS’s Superior Copy Tool

ditto is Apple’s copy utility, designed for macOS-specific needs:

# Basic copy (preserves everything)
$ ditto source.txt dest.txt
$ ditto source_dir/ dest_dir/

# Copy with verbose output
$ ditto -V source_dir/ dest_dir/

# Preserve extended attributes and ACLs (default)
$ ditto source/ dest/

# Flatten to tar archive
$ ditto -c -k --sequesterRsrc source_dir/ archive.zip

# Extract from archive
$ ditto -x -k archive.zip dest_dir/

Why Use ditto Over cp?

# ditto advantages:
# 1. Properly handles resource forks
# 2. Preserves HFS+ metadata
# 3. Can create/extract ZIP archives
# 4. Works correctly with bundles (apps)

# Copy an application bundle
$ ditto /Applications/TextEdit.app ~/Desktop/TextEdit.app

# cp -r might miss resource forks or metadata
# ditto is the safer choice for macOS files

Creating Archives with ditto

# Create ZIP archive
$ ditto -c -k --sequesterRsrc --keepParent source_dir/ archive.zip

# Options:
#   -c : create archive
#   -k : use PKZip format
#   --sequesterRsrc : store resource forks in __MACOSX
#   --keepParent : include parent directory in archive

# Create archive without __MACOSX metadata
$ ditto -c -k --norsrc source_dir/ archive.zip

rsync: Synchronization

macOS includes rsync, but it’s an older version. The Homebrew version has more features:

# Check version
$ rsync --version
rsync  version 2.6.9  protocol version 29    # System version (old)

# Install newer version
$ brew install rsync
$ /opt/homebrew/bin/rsync --version
rsync  version 3.2.7  protocol version 31    # Much newer

Basic Synchronization

# Sync directories
$ rsync -av source/ dest/

# Options breakdown:
#   -a : archive mode (preserves permissions, timestamps, etc.)
#   -v : verbose

# With progress
$ rsync -av --progress source/ dest/

# Dry run (show what would happen)
$ rsync -av --dry-run source/ dest/

# Delete files in dest that aren't in source
$ rsync -av --delete source/ dest/

Preserving macOS Metadata

# System rsync doesn't have extended attribute support
# Homebrew rsync does

# Preserve extended attributes
$ rsync -avX source/ dest/    # -X for extended attributes

# Full macOS preservation (Homebrew rsync 3.x)
$ rsync -av --xattrs --fileflags source/ dest/

# For cross-platform transfers, you might want to strip metadata
$ rsync -av --no-xattrs source/ dest/

Remote Synchronization

# Copy to remote (SSH)
$ rsync -av source/ user@remote:/path/to/dest/

# Copy from remote
$ rsync -av user@remote:/path/to/source/ dest/

# With compression for slow links
$ rsync -avz source/ user@remote:/dest/

# Limit bandwidth (KB/s)
$ rsync -av --bwlimit=1000 source/ user@remote:/dest/

# Resume interrupted transfer
$ rsync -av --partial --progress source/ dest/

# Combine for reliable large transfers
$ rsync -avz --partial --progress --bwlimit=5000 source/ user@remote:/dest/

Excluding Files

# Exclude patterns
$ rsync -av --exclude='*.log' --exclude='.DS_Store' source/ dest/

# Exclude from file
$ rsync -av --exclude-from='exclude.txt' source/ dest/

# Common exclusions for macOS
$ rsync -av \
    --exclude='.DS_Store' \
    --exclude='._*' \
    --exclude='.Spotlight-*' \
    --exclude='.Trashes' \
    --exclude='.fseventsd' \
    source/ dest/

Extended Attributes (xattr)

macOS stores additional metadata in extended attributes:

# List extended attributes
$ xattr file.txt
com.apple.quarantine

# View attribute content
$ xattr -p com.apple.quarantine file.txt
0083;5f3e8bc0;Chrome;XXXXXXXX

# Remove quarantine attribute (common need for downloaded files)
$ xattr -d com.apple.quarantine file.txt

# Remove all extended attributes
$ xattr -c file.txt

# Recursive operations
$ xattr -r -d com.apple.quarantine app_folder/

# Copy preserving xattrs
$ cp -p file.txt dest.txt    # -p preserves xattrs

# List with details
$ xattr -l file.txt
com.apple.quarantine: 0083;5f3e8bc0;Chrome;XXXXXXXX
com.apple.lastuseddate#PS: ... (binary data)

Common Extended Attributes

AttributePurpose
com.apple.quarantineDownloaded file, triggers Gatekeeper
com.apple.lastuseddate#PSLast opened date
com.apple.metadata:kMDItemWhereFromsDownload URL
com.apple.FinderInfoFinder metadata (labels, etc.)
com.apple.ResourceForkClassic resource fork data

Stripping Metadata for Transfer

# Remove all macOS metadata before sharing
$ xattr -cr directory/

# Or use tar with no extended attributes
$ COPYFILE_DISABLE=1 tar cvf archive.tar directory/

# Copy without metadata using rsync
$ rsync -av --no-xattrs source/ dest/

find: Finding Files

Basic Usage (Mostly Compatible)

# Find by name (compatible)
$ find . -name "*.txt"

# Find by type (compatible)
$ find . -type f    # Files
$ find . -type d    # Directories

# Find by modification time (compatible)
$ find . -mtime -7    # Modified in last 7 days

# Find by size (compatible)
$ find . -size +100M    # Larger than 100MB

# Execute command on results (compatible)
$ find . -name "*.tmp" -exec rm {} \;

# Delete found files (compatible)
$ find . -name "*.tmp" -delete

macOS-Specific Options

# Extended regex (BSD find)
$ find -E . -regex ".*\.(jpg|png|gif)"

# GNU find uses -regextype
$ find . -regextype posix-extended -regex ".*\.(jpg|png|gif)"

# Find with extended attributes
$ find . -xattrname com.apple.quarantine

# Find files by Spotlight metadata
$ mdfind -onlyin . "kMDItemFSSize > 1000000"

Handling Spaces in Filenames

# Using -print0 and xargs -0 (compatible)
$ find . -name "*.txt" -print0 | xargs -0 rm

# Using -exec (compatible)
$ find . -name "*.txt" -exec rm {} \;

mkdir: Creating Directories

# Create directory (compatible)
$ mkdir newdir

# Create parent directories as needed (compatible)
$ mkdir -p path/to/new/dir

# Set permissions on creation (compatible)
$ mkdir -m 755 newdir

# Verbose (compatible)
$ mkdir -v newdir
# Create hard link (compatible)
$ ln original.txt hardlink.txt

# Create symbolic link (compatible)
$ ln -s /path/to/original symlink

# Force overwrite existing link (compatible)
$ ln -sf /new/target symlink

# Create relative symbolic link
$ ln -s ../sibling/file.txt symlink
# macOS symbolic links work across volumes
$ ln -s /Volumes/External/file.txt local_link

# Aliases vs Symbolic Links
# Finder aliases are NOT symbolic links
# They track file moves; symlinks don't

# Create Finder alias from command line
$ osascript -e 'tell application "Finder" to make alias file to POSIX file "/path/to/original" at POSIX file "/path/to/location"'

chmod, chown: Permissions

Basic Usage (Compatible)

# Change permissions (compatible)
$ chmod 755 script.sh
$ chmod u+x script.sh
$ chmod -R 644 directory/

# Change owner (compatible)
$ sudo chown user:group file.txt
$ sudo chown -R user:group directory/

macOS Specific

# macOS also has ACLs
$ ls -le file.txt    # Shows ACL entries

# Remove ACLs
$ chmod -N file.txt

# View with full permissions
$ ls -l@ file.txt    # Shows extended attributes

Touch: Create/Update Timestamps

# Create empty file or update timestamp (compatible)
$ touch file.txt

# Set specific modification time (BSD syntax)
$ touch -t 202401151200 file.txt    # YYYYMMDDhhmm

# Use another file's timestamp (compatible)
$ touch -r reference.txt target.txt

# Only update if file exists (compatible)
$ touch -c file.txt

Practical Examples

Backup a Directory Preserving Metadata

# Best method on macOS
$ ditto -V source_dir/ backup_dir/

# Or with rsync
$ rsync -av --progress source_dir/ backup_dir/

Clean Up .DS_Store Files

# Find and delete
$ find . -name ".DS_Store" -delete

# Also remove ._* AppleDouble files
$ find . -name "._*" -delete

# Prevent .DS_Store on network volumes
$ defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true

Remove Quarantine from Downloads

# Single file
$ xattr -d com.apple.quarantine download.dmg

# Entire directory
$ xattr -dr com.apple.quarantine ~/Downloads/

# From an app
$ xattr -cr /Applications/SomeApp.app

Sync with External Drive

# Initial sync
$ rsync -av --progress ~/Documents/ /Volumes/Backup/Documents/

# Update (only changed files)
$ rsync -av --progress --delete ~/Documents/ /Volumes/Backup/Documents/

Find Large Files

# Using find
$ find ~ -type f -size +100M 2>/dev/null

# Using Spotlight (faster)
$ mdfind -onlyin ~ "kMDItemFSSize > 104857600"

# Sorted by size
$ find ~ -type f -size +100M -exec ls -lh {} \; 2>/dev/null | sort -k5 -h

Summary: macOS File Operations

TaskBest CommandNotes
Copy filesditto or cp -pditto preserves everything
Sync directoriesrsync -avUse Homebrew rsync for xattr support
Large file copyrsync --progressShows progress
Move to Trashtrash (brew)Safer than rm
Remove quarantinexattr -d com.apple.quarantineCommon need
Archive directoryditto -c -kCreates ZIP with metadata
Strip metadataxattr -crBefore sharing
Find filesmdfindUses Spotlight, much faster

Understanding these tools and their macOS-specific behaviors helps you manage files effectively while preserving the metadata that macOS applications expect.

Networking Commands

Network diagnostics on macOS use a mix of traditional BSD tools and Apple-specific utilities. If you’re coming from Linux, you’ll find that ip doesn’t exist, but many BSD alternatives are available. This chapter covers network commands for troubleshooting and configuration.

Interface Configuration

ifconfig vs ip

Linux uses ip, but macOS uses ifconfig:

# Linux
$ ip addr show
$ ip link show
$ ip route show

# macOS - ifconfig is the primary tool
$ ifconfig
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	options=6463<RXCSUM,TXCSUM,TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM>
	ether aa:bb:cc:dd:ee:ff
	inet6 fe80::1c1c:abcd:1234:5678%en0 prefixlen 64 secured scopeid 0x8
	inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255
	nd6 options=201<PERFORMNUD,DAD>
	media: autoselect
	status: active

Common ifconfig operations:

# List all interfaces
$ ifconfig -a

# Show specific interface
$ ifconfig en0

# Get just the IP address (parsing)
$ ifconfig en0 | grep "inet " | awk '{print $2}'
192.168.1.100

# Show only active interfaces
$ ifconfig | grep -E "^[a-z]" | cut -d: -f1
lo0
en0
en1

# Enable/disable interface (requires sudo)
$ sudo ifconfig en0 down
$ sudo ifconfig en0 up

# Set IP address manually (temporary)
$ sudo ifconfig en0 inet 192.168.1.50 netmask 255.255.255.0

# Add an alias IP
$ sudo ifconfig en0 alias 192.168.1.51 netmask 255.255.255.0

Understanding macOS Interface Names

lo0     - Loopback (127.0.0.1)
en0     - Primary Ethernet or Wi-Fi (depends on Mac model)
en1     - Secondary network interface
en2-enX - Additional interfaces (USB Ethernet, etc.)
bridge0 - Network bridge
awdl0   - Apple Wireless Direct Link (AirDrop, etc.)
llw0    - Low-latency Wi-Fi (various Apple services)
utun0-X - VPN tunnel interfaces
gif0    - Generic tunnel interface
stf0    - 6to4 tunnel interface

Find your primary interface:

# Get active interface
$ route get default | grep interface
    interface: en0

# Or using networksetup
$ networksetup -listallhardwareports | grep -A1 "Wi-Fi"
Hardware Port: Wi-Fi
Device: en0

networksetup: macOS Network Configuration

networksetup is the command-line equivalent of System Preferences > Network:

# List all network services
$ networksetup -listallnetworkservices
An asterisk (*) denotes that a network service is disabled.
Wi-Fi
USB 10/100/1000 LAN
Thunderbolt Bridge
iPhone USB

# List hardware ports and devices
$ networksetup -listallhardwareports
Hardware Port: Wi-Fi
Device: en0
Ethernet Address: aa:bb:cc:dd:ee:ff

Hardware Port: Thunderbolt 1
Device: en1
...

# Get detailed info for a service
$ networksetup -getinfo "Wi-Fi"
DHCP Configuration
IP address: 192.168.1.100
Subnet mask: 255.255.255.0
Router: 192.168.1.1
Client ID:
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Wi-Fi ID: aa:bb:cc:dd:ee:ff

IP Configuration

# Set to DHCP
$ sudo networksetup -setdhcp "Wi-Fi"

# Set static IP
$ sudo networksetup -setmanual "Wi-Fi" 192.168.1.50 255.255.255.0 192.168.1.1
#                               service    IP            netmask       gateway

# Get current IP configuration
$ networksetup -getinfo "Wi-Fi"

DNS Configuration

# View current DNS servers
$ networksetup -getdnsservers "Wi-Fi"
8.8.8.8
8.8.4.4

# Set DNS servers
$ sudo networksetup -setdnsservers "Wi-Fi" 8.8.8.8 8.8.4.4

# Clear DNS (use DHCP-provided)
$ sudo networksetup -setdnsservers "Wi-Fi" empty

# Flush DNS cache
$ sudo dscacheutil -flushcache
$ sudo killall -HUP mDNSResponder

Wi-Fi Management

# Get Wi-Fi power state
$ networksetup -getairportpower en0
Wi-Fi Power (en0): On

# Turn Wi-Fi on/off
$ networksetup -setairportpower en0 off
$ networksetup -setairportpower en0 on

# Get current Wi-Fi network
$ networksetup -getairportnetwork en0
Current Wi-Fi Network: MyNetwork

# Connect to a Wi-Fi network
$ networksetup -setairportnetwork en0 "NetworkName" "password"

# List preferred (known) networks
$ networksetup -listpreferredwirelessnetworks en0
Preferred networks on en0:
	MyHomeNetwork
	OfficeNetwork
	CoffeeShopWiFi

# Remove a preferred network
$ sudo networksetup -removepreferredwirelessnetwork en0 "CoffeeShopWiFi"

Proxy Configuration

# Get proxy settings
$ networksetup -getwebproxy "Wi-Fi"
Enabled: No
Server:
Port: 0
Authenticated Proxy Enabled: 0

# Set web proxy
$ sudo networksetup -setwebproxy "Wi-Fi" proxy.example.com 8080

# Set with authentication
$ sudo networksetup -setwebproxy "Wi-Fi" proxy.example.com 8080 on user password

# Disable proxy
$ sudo networksetup -setwebproxystate "Wi-Fi" off

# Set proxy auto-config (PAC) URL
$ sudo networksetup -setautoproxyurl "Wi-Fi" "http://example.com/proxy.pac"

scutil: System Configuration

scutil provides low-level access to system configuration, including network settings:

# Get DNS configuration
$ scutil --dns
DNS configuration

resolver #1
  search domain[0] : home
  nameserver[0] : 192.168.1.1
  nameserver[1] : 8.8.8.8
  if_index : 8 (en0)
  flags    : Request A records
  reach    : 0x00000002 (Reachable)
...

# Check host reachability
$ scutil -r google.com
Reachable

$ scutil -r 192.168.1.1
Reachable,Direct

# Get/set hostname
$ scutil --get HostName
my-macbook

$ scutil --get LocalHostName
my-macbook

$ scutil --get ComputerName
My MacBook Pro

# Set hostname (requires sudo)
$ sudo scutil --set HostName newhostname
$ sudo scutil --set LocalHostName newhostname
$ sudo scutil --set ComputerName "New MacBook Pro"

# Get proxy configuration
$ scutil --proxy
<dictionary> {
  ExceptionsList : <array> {
    0 : *.local
    1 : 169.254/16
  }
  FTPPassive : 1
  HTTPEnable : 0
  HTTPSEnable : 0
}

Interactive mode for exploring configuration:

$ scutil
> list
  subKey [0] = Plugin:IPConfiguration
  subKey [1] = Plugin:InterfaceNamer
  ...
> show State:/Network/Global/IPv4
<dictionary> {
  PrimaryInterface : en0
  PrimaryService : XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
  Router : 192.168.1.1
}
> show State:/Network/Interface/en0/IPv4
<dictionary> {
  Addresses : <array> {
    0 : 192.168.1.100
  }
  BroadcastAddresses : <array> {
    0 : 192.168.1.255
  }
  SubnetMasks : <array> {
    0 : 255.255.255.0
  }
}
> quit

airport: Wi-Fi Diagnostics

The airport utility is hidden but powerful:

# Create an alias (the path is long)
$ alias airport='/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'

# Show current connection info
$ airport -I
     agrCtlRSSI: -52
     agrExtRSSI: 0
    agrCtlNoise: -88
    agrExtNoise: 0
          state: running
        op mode: station
     lastTxRate: 866
        maxRate: 1200
lastAssocStatus: 0
    802.11 auth: open
      link auth: wpa2-psk
          BSSID: aa:bb:cc:dd:ee:ff
           SSID: MyNetwork
            MCS: 9
  guardInterval: 800
            NSS: 2
        channel: 149,80

# Scan for available networks
$ airport -s
                            SSID BSSID             RSSI CHANNEL HT CC SECURITY
                       MyNetwork aa:bb:cc:dd:ee:ff -52  149,+1  Y  US WPA2(PSK/AES/AES)
                   Neighbor_WiFi bb:cc:dd:ee:ff:00 -68  6       Y  US WPA2(PSK/AES/AES)
                   Guest_Network cc:dd:ee:ff:00:11 -75  11      Y  -- WPA2(PSK/AES/AES)

# Disconnect from current network
$ sudo airport -z

# Show supported channels
$ airport -c
Supported channels:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 36, 40, 44, 48, 52, 56, 60, 64, 100, ...

netstat: Network Statistics

# Show all connections
$ netstat -an

# Show listening ports
$ netstat -an | grep LISTEN

# Show routing table
$ netstat -rn
Routing tables

Internet:
Destination        Gateway            Flags        Netif Expire
default            192.168.1.1        UGScg          en0
127.0.0.1          127.0.0.1          UH             lo0
192.168.1          link#8             UCS            en0      !
192.168.1.1        aa:bb:cc:dd:ee:ff  UHLWIir        en0   1198
192.168.1.100      127.0.0.1          UHS            lo0

# Show per-protocol statistics
$ netstat -s

# Show statistics for specific protocol
$ netstat -s -p tcp
$ netstat -s -p udp

# Show interface statistics
$ netstat -i
Name  Mtu   Network       Address            Ipkts Ierrs    Opkts Oerrs  Coll
lo0   16384 <Link#1>                        123456     0   123456     0     0
lo0   16384 127           localhost         123456     -   123456     -     -
en0   1500  <Link#8>    aa:bb:cc:dd:ee:ff  987654     0   654321     0     0
en0   1500  192.168.1     192.168.1.100    987654     -   654321     -     -

Linux netstat equivalents:

# Linux: ss -tuln
# macOS: netstat -an (no ss command)

# Linux: ss -tunp
# macOS: lsof -i (shows processes)

# Show processes using network
$ lsof -i -P | head -20
COMMAND     PID   USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
loginwindow 123   user    6u  IPv4   0x1234      0t0  UDP *:*
Google      456   user   23u  IPv4   0x5678      0t0  TCP 192.168.1.100:51234->142.250.x.x:443 (ESTABLISHED)

lsof: List Open Files (Including Network)

# Show all network connections
$ lsof -i

# Show listening ports
$ lsof -i -P | grep LISTEN
rapportd    123 user    4u  IPv4 0x1234    0t0  TCP *:49152 (LISTEN)
rapportd    123 user    5u  IPv6 0x5678    0t0  TCP *:49152 (LISTEN)

# Show connections on specific port
$ lsof -i :80
$ lsof -i :443

# Show connections to specific host
$ lsof -i @google.com

# Show TCP connections only
$ lsof -i TCP

# Show UDP connections only
$ lsof -i UDP

# Show process using a port
$ lsof -i :8080 -P
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
node    1234 user   23u  IPv4 0x1234      0t0  TCP *:8080 (LISTEN)

# Kill process on a port
$ lsof -ti :8080 | xargs kill

Routing

# Show routing table
$ netstat -rn

# Or using route
$ route -n get default
   route to: default
destination: default
       mask: default
    gateway: 192.168.1.1
  interface: en0
      flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>

# Add a route
$ sudo route add -net 10.0.0.0/8 192.168.1.254

# Delete a route
$ sudo route delete -net 10.0.0.0/8

# Flush routing table (careful!)
$ sudo route flush

ping and traceroute

# Ping (compatible with Linux)
$ ping -c 5 google.com
PING google.com (142.250.x.x): 56 data bytes
64 bytes from 142.250.x.x: icmp_seq=0 ttl=117 time=12.345 ms
...

# Flood ping (requires sudo)
$ sudo ping -f google.com

# Traceroute
$ traceroute google.com
traceroute to google.com (142.250.x.x), 64 hops max, 52 byte packets
 1  192.168.1.1 (192.168.1.1)  1.234 ms  0.987 ms  0.876 ms
 2  isp-gateway (10.0.0.1)  5.432 ms  4.321 ms  4.567 ms
...

# Use ICMP instead of UDP (requires sudo)
$ sudo traceroute -I google.com

DNS Tools

# DNS lookup
$ host google.com
google.com has address 142.250.x.x
google.com has IPv6 address 2607:f8b0:...
google.com mail is handled by 10 smtp.google.com.

# More detailed DNS lookup
$ dig google.com
; <<>> DiG 9.10.6 <<>> google.com
;; ANSWER SECTION:
google.com.		123	IN	A	142.250.x.x

# Query specific record types
$ dig MX google.com
$ dig NS google.com
$ dig TXT google.com
$ dig AAAA google.com    # IPv6

# Query specific DNS server
$ dig @8.8.8.8 google.com

# Reverse DNS lookup
$ dig -x 8.8.8.8

# nslookup (also available)
$ nslookup google.com
Server:		192.168.1.1
Address:	192.168.1.1#53

Non-authoritative answer:
Name:	google.com
Address: 142.250.x.x

# dscacheutil for local DNS cache
$ dscacheutil -q host -a name google.com
name: google.com
ip_address: 142.250.x.x

# Flush DNS cache
$ sudo dscacheutil -flushcache
$ sudo killall -HUP mDNSResponder

curl and wget

Both are available (wget via Homebrew):

# curl is pre-installed
$ curl -I https://apple.com
HTTP/2 200
content-type: text/html; charset=utf-8
...

# Download file
$ curl -O https://example.com/file.zip
$ curl -o localname.zip https://example.com/file.zip

# Follow redirects
$ curl -L https://example.com/redirect

# Show headers and body
$ curl -i https://example.com

# POST request
$ curl -X POST -d "key=value" https://api.example.com

# JSON POST
$ curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' https://api.example.com

# wget (install via Homebrew)
$ brew install wget
$ wget https://example.com/file.zip
$ wget -c https://example.com/largefile.zip    # Resume download
$ wget -r -l 2 https://example.com             # Recursive, 2 levels deep

nc (netcat): Network Swiss Army Knife

# Test if port is open
$ nc -zv google.com 443
Connection to google.com port 443 [tcp/https] succeeded!

# Port scanning
$ nc -zv 192.168.1.1 20-25
Connection to 192.168.1.1 port 22 [tcp/ssh] succeeded!

# Create simple TCP server
$ nc -l 8080

# Connect to server
$ nc localhost 8080

# Transfer file
# On receiver:
$ nc -l 8080 > received_file.txt
# On sender:
$ nc destination 8080 < file_to_send.txt

# Chat between two machines
# Machine 1:
$ nc -l 8080
# Machine 2:
$ nc machine1 8080

# HTTP request
$ echo -e "GET / HTTP/1.1\nHost: example.com\n\n" | nc example.com 80

arp: Address Resolution Protocol

# Show ARP cache
$ arp -a
? (192.168.1.1) at aa:bb:cc:dd:ee:ff on en0 ifscope [ethernet]
? (192.168.1.100) at 11:22:33:44:55:66 on en0 ifscope permanent [ethernet]

# Delete an entry
$ sudo arp -d 192.168.1.50

# Add a static entry
$ sudo arp -s 192.168.1.50 aa:bb:cc:dd:ee:ff

Packet Capture: tcpdump

# Capture on interface (requires sudo)
$ sudo tcpdump -i en0

# Capture specific host
$ sudo tcpdump -i en0 host 192.168.1.50

# Capture specific port
$ sudo tcpdump -i en0 port 80
$ sudo tcpdump -i en0 port 443

# Capture and save to file
$ sudo tcpdump -i en0 -w capture.pcap

# Read capture file
$ tcpdump -r capture.pcap

# Show packet contents
$ sudo tcpdump -i en0 -X

# Capture HTTP traffic
$ sudo tcpdump -i en0 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

Network Diagnostics Tool

macOS has a built-in network diagnostics:

# Run network diagnostics (creates report)
$ /System/Library/CoreServices/Applications/Network\ Utility.app/Contents/MacOS/Network\ Utility

# Or use networkQuality (macOS 12+)
$ networkQuality
==== SUMMARY ====
Upload capacity: 50.123 Mbps
Download capacity: 200.456 Mbps
Upload flows: 12
Download flows: 16
Responsiveness: High (1234 RPM)

# Run in sequential mode (more accurate)
$ networkQuality -s

Summary: Linux to macOS Mapping

Linux CommandmacOS EquivalentNotes
ip addrifconfigifconfig is deprecated on Linux
ip linkifconfigSame
ip routenetstat -rn or route
ssnetstat or lsof -i
systemctl restart networknetworksetupNo systemd on macOS
nmclinetworksetupmacOS equivalent
iwconfig/iwlistairportWi-Fi utility
hostnamectlscutil --get/set HostName
resolvectlscutil --dns

Key macOS-specific tools:

  • networksetup: High-level network configuration
  • scutil: System configuration access
  • airport: Wi-Fi diagnostics
  • dscacheutil: Directory services cache
  • networkQuality: Network speed test (macOS 12+)

System Information Commands

macOS provides several ways to query system information from the command line. Some commands are BSD standards, others are macOS-specific. This chapter covers the essential tools for gathering system details.

sw_vers: macOS Version

The simplest way to get macOS version information:

$ sw_vers
ProductName:		macOS
ProductVersion:		14.2.1
BuildVersion:		23C71

# Individual values
$ sw_vers -productName
macOS

$ sw_vers -productVersion
14.2.1

$ sw_vers -buildVersion
23C71

Script usage:

# Check minimum version
MACOS_VERSION=$(sw_vers -productVersion)
MAJOR_VERSION=$(echo "$MACOS_VERSION" | cut -d. -f1)

if [ "$MAJOR_VERSION" -lt 13 ]; then
    echo "This script requires macOS 13 or later"
    exit 1
fi

# Version comparison function
version_gte() {
    [ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" = "$2" ]
}

if version_gte "$MACOS_VERSION" "14.0"; then
    echo "Running macOS Sonoma or later"
fi

uname: System Name and Architecture

Standard Unix command available on all Unix-like systems:

$ uname -a
Darwin MacBook-Pro.local 23.2.0 Darwin Kernel Version 23.2.0: Wed Nov 15 21:53:18 PST 2023; root:xnu-10002.61.3~2/RELEASE_ARM64_T6000 arm64

# Individual options
$ uname -s    # Kernel name
Darwin

$ uname -n    # Network hostname
MacBook-Pro.local

$ uname -r    # Kernel release
23.2.0

$ uname -v    # Kernel version (verbose)
Darwin Kernel Version 23.2.0: Wed Nov 15 21:53:18 PST 2023; root:xnu-10002.61.3~2/RELEASE_ARM64_T6000

$ uname -m    # Machine hardware
arm64

$ uname -p    # Processor type
arm

Detect Apple Silicon vs Intel:

# Check architecture
ARCH=$(uname -m)
if [ "$ARCH" = "arm64" ]; then
    echo "Apple Silicon"
elif [ "$ARCH" = "x86_64" ]; then
    echo "Intel"
fi

# Check if running under Rosetta 2
if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then
    echo "Running under Rosetta 2 (Intel emulation)"
fi

hostinfo: Detailed Host Information

macOS-specific command for host details:

$ hostinfo
Mach kernel version:
	 Darwin Kernel Version 23.2.0: Wed Nov 15 21:53:18 PST 2023; root:xnu-10002.61.3~2/RELEASE_ARM64_T6000
Kernel configured for up to 10 processors.
10 processors are physically available.
10 processors are logically available.
Processor type: arm64e (ARM64E)
Processors active: 0 1 2 3 4 5 6 7 8 9
Primary memory available: 32.00 gigabytes
Default processor set: 436 tasks, 2847 threads, 10 processors
Load average: 2.15, Mach factor: 7.84

sysctl: Kernel Parameters

Query (and set) kernel parameters:

# Show all parameters
$ sysctl -a

# Common queries
$ sysctl hw.model
hw.model: Mac14,6

$ sysctl hw.ncpu
hw.ncpu: 10

$ sysctl hw.memsize
hw.memsize: 34359738368

$ sysctl hw.physicalcpu
hw.physicalcpu: 10

$ sysctl hw.logicalcpu
hw.logicalcpu: 10

$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro

Hardware Information

# Memory
$ sysctl hw.memsize                    # Total memory in bytes
hw.memsize: 34359738368

$ echo "$(( $(sysctl -n hw.memsize) / 1073741824 )) GB"
32 GB

# CPU
$ sysctl -n hw.ncpu                    # Number of CPUs
10

$ sysctl -n hw.physicalcpu             # Physical CPU cores
10

$ sysctl -n machdep.cpu.brand_string   # CPU model (Intel only)
# On Apple Silicon, this returns empty or fails

# Machine model
$ sysctl -n hw.model
Mac14,6

# L1/L2/L3 Cache
$ sysctl -n hw.l1icachesize            # L1 instruction cache
131072

$ sysctl -n hw.l1dcachesize            # L1 data cache
65536

$ sysctl -n hw.l2cachesize             # L2 cache
4194304

Kernel and System

# Kernel version
$ sysctl kern.version
kern.version: Darwin Kernel Version 23.2.0...

$ sysctl kern.ostype
kern.ostype: Darwin

$ sysctl kern.osrelease
kern.osrelease: 23.2.0

# Hostname
$ sysctl kern.hostname
kern.hostname: MacBook-Pro.local

# Boot time
$ sysctl kern.boottime
kern.boottime: { sec = 1705123456, usec = 0 } Sat Jan 13 10:00:00 2024

# Uptime
$ sysctl -n kern.boottime | awk '{print $4}' | cut -d, -f1
# Then calculate difference from current time

Network Parameters

# Maximum socket buffer
$ sysctl net.inet.tcp.sendspace
net.inet.tcp.sendspace: 131072

$ sysctl net.inet.tcp.recvspace
net.inet.tcp.recvspace: 131072

# View all network parameters
$ sysctl net.inet.tcp

Setting Parameters

# Set parameter (requires sudo, may require SIP disabled)
$ sudo sysctl -w kern.maxfiles=65536
$ sudo sysctl -w kern.maxfilesperproc=65536

# Persist changes (create file in /etc/sysctl.conf)
$ echo "kern.maxfiles=65536" | sudo tee -a /etc/sysctl.conf

system_profiler: Comprehensive System Information

The command-line equivalent of “About This Mac” and System Information app:

# All information (very verbose, takes time)
$ system_profiler

# List available data types
$ system_profiler -listDataTypes
SPParallelATADataType
SPUniversalAccessDataType
SPSecureElementDataType
SPApplicationsDataType
SPAudioDataType
SPBluetoothDataType
...

# Specific data type
$ system_profiler SPHardwareDataType
Hardware:

    Hardware Overview:

      Model Name: MacBook Pro
      Model Identifier: Mac14,6
      Model Number: MNWA3LL/A
      Chip: Apple M2 Pro
      Total Number of Cores: 10 (6 performance and 4 efficiency)
      Memory: 32 GB
      System Firmware Version: 10151.61.4
      OS Loader Version: 10151.61.4
      Serial Number (system): XXXXXXXXXXXX
      Hardware UUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
      Provisioning UDID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
      Activation Lock Status: Enabled

Common Data Types

# Hardware overview
$ system_profiler SPHardwareDataType

# Software overview
$ system_profiler SPSoftwareDataType
Software:

    System Software Overview:

      System Version: macOS 14.2.1 (23C71)
      Kernel Version: Darwin 23.2.0
      Boot Volume: Macintosh HD
      Boot Mode: Normal
      Computer Name: MacBook Pro
      User Name: John Doe (john)
      Secure Virtual Memory: Enabled
      System Integrity Protection: Enabled
      Time since boot: 3 days, 2:15

# Memory slots
$ system_profiler SPMemoryDataType

# Storage
$ system_profiler SPStorageDataType
Storage:

    Macintosh HD:

      Available: 234.5 GB (234,500,000,000 bytes)
      Capacity: 494.38 GB (494,380,000,000 bytes)
      File System: APFS
      Writable: Yes
      BSD Name: disk3s5

# Network interfaces
$ system_profiler SPNetworkDataType

# USB devices
$ system_profiler SPUSBDataType

# Displays
$ system_profiler SPDisplaysDataType

# Audio
$ system_profiler SPAudioDataType

# Bluetooth
$ system_profiler SPBluetoothDataType

# Power/Battery
$ system_profiler SPPowerDataType
Power:

    Battery Information:

      Model Information:
          Serial Number: XXXXXXXXXXXX
          Manufacturer: Apple
          Device Name: bq20z451
          ...
      Charge Information:
          Charge Remaining (mAh): 4500
          State of Charge (%): 78
          ...
      Health Information:
          Cycle Count: 125
          Condition: Normal
          Maximum Capacity: 95%

Output Formats

# XML output (for parsing)
$ system_profiler SPHardwareDataType -xml

# JSON output (macOS 10.15+)
$ system_profiler SPHardwareDataType -json

# Parse JSON with jq
$ system_profiler SPHardwareDataType -json | jq '.SPHardwareDataType[0].chip_type'
"Apple M2 Pro"

Useful Queries

# Get serial number
$ system_profiler SPHardwareDataType | grep "Serial Number (system)"
      Serial Number (system): XXXXXXXXXXXX

# Or using ioreg
$ ioreg -l | grep IOPlatformSerialNumber | awk -F'"' '{print $4}'
XXXXXXXXXXXX

# Get model identifier
$ system_profiler SPHardwareDataType | grep "Model Identifier"
      Model Identifier: Mac14,6

# Get macOS version
$ system_profiler SPSoftwareDataType | grep "System Version"
      System Version: macOS 14.2.1 (23C71)

# Get battery cycle count
$ system_profiler SPPowerDataType | grep "Cycle Count"
          Cycle Count: 125

# List installed applications
$ system_profiler SPApplicationsDataType

# Get display resolution
$ system_profiler SPDisplaysDataType | grep Resolution
          Resolution: 3456 x 2234 Retina

ioreg: I/O Registry

The I/O Registry contains hardware and driver information:

# List all devices
$ ioreg -l

# Find specific device
$ ioreg -l | grep -i bluetooth

# Get battery info
$ ioreg -l -n AppleSmartBattery | grep -E "Capacity|CycleCount|Temperature"
    "AppleRawMaxCapacity" = 4500
    "AppleRawCurrentCapacity" = 3510
    "CycleCount" = 125
    "Temperature" = 2987

# Get serial number
$ ioreg -l | grep IOPlatformSerialNumber

# Power adapter info
$ ioreg -l -n AppleSmartBattery | grep -E "ExternalConnected|Charging"
    "ExternalConnected" = Yes
    "IsCharging" = No

# Display information
$ ioreg -l -w 0 | grep -i "IODisplayPrefs" -A 20

df and du: Disk Space

# Disk free space
$ df -h
Filesystem       Size   Used  Avail Capacity iused ifree %iused  Mounted on
/dev/disk3s1s1  466Gi   15Gi  234Gi     6%  500000  9999999999    0%   /
/dev/disk3s5    466Gi  200Gi  234Gi    46% 1234567  9999999999    0%   /System/Volumes/Data

# Specific filesystem
$ df -h /

# Disk usage of directory
$ du -sh ~/Documents
15G	/Users/john/Documents

# Sort by size
$ du -sh * | sort -hr | head -10

# Include hidden files
$ du -sh .[^.]* * 2>/dev/null | sort -hr | head -10

vm_stat: Virtual Memory Statistics

$ vm_stat
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free:                               12345.
Pages active:                            234567.
Pages inactive:                          123456.
Pages speculative:                        12345.
Pages throttled:                              0.
Pages wired down:                        234567.
Pages purgeable:                          12345.
"Translation faults":                 123456789.
Pages copy-on-write:                   12345678.
Pages zero filled:                     12345678.
Pages reactivated:                       123456.
Pages purged:                            123456.
File-backed pages:                       234567.
Anonymous pages:                         234567.
Pages stored in compressor:              123456.
Pages occupied by compressor:             12345.
Decompressions:                          123456.
Compressions:                            234567.
Pageins:                                 123456.
Pageouts:                                  1234.
Swapins:                                      0.
Swapouts:                                     0.

Convert to human-readable:

# Function to show memory in GB
meminfo() {
    local pagesize=$(vm_stat | head -1 | grep -oE '[0-9]+')
    local free=$(vm_stat | grep "Pages free" | awk '{print $3}' | tr -d '.')
    local active=$(vm_stat | grep "Pages active" | awk '{print $3}' | tr -d '.')
    local inactive=$(vm_stat | grep "Pages inactive" | awk '{print $3}' | tr -d '.')
    local wired=$(vm_stat | grep "Pages wired" | awk '{print $4}' | tr -d '.')

    echo "Free:     $(echo "scale=2; $free * $pagesize / 1073741824" | bc) GB"
    echo "Active:   $(echo "scale=2; $active * $pagesize / 1073741824" | bc) GB"
    echo "Inactive: $(echo "scale=2; $inactive * $pagesize / 1073741824" | bc) GB"
    echo "Wired:    $(echo "scale=2; $wired * $pagesize / 1073741824" | bc) GB"
}

top and htop: Process Information

# top (pre-installed)
$ top
# Press 'q' to quit

# Sort by CPU
$ top -o cpu

# Sort by memory
$ top -o rsize

# Show specific user
$ top -U username

# Non-interactive, show once
$ top -l 1

# Show process statistics
$ top -l 1 -n 0 | head -12
Processes: 456 total, 3 running, 453 sleeping, 2345 threads
Load Avg: 2.15, 2.34, 2.45
CPU usage: 5.0% user, 3.0% sys, 92.0% idle
SharedLibs: 300M resident, 50M data, 20M linkedit.
MemRegions: 123456 total, 5G resident, 200M private, 2G shared.
PhysMem: 20G used (2G wired, 10G compressor), 12G unused.
VM: 5T vsize, 3G swapins, 0B swapouts.
Networks: packets: 1234567/1G in, 123456/500M out.
Disks: 1234567/30G read, 234567/20G written.

For a better experience:

# Install htop
$ brew install htop
$ htop

ps: Process Status

# All processes (BSD style)
$ ps aux

# All processes (System V style)
$ ps -ef

# Process tree
$ ps -ejH

# Find specific process
$ ps aux | grep [n]ginx

# Show processes by memory usage
$ ps aux --sort=-%mem | head -10

# Show processes by CPU usage
$ ps aux --sort=-%cpu | head -10

# Show specific columns
$ ps -eo pid,ppid,user,%cpu,%mem,comm | head -10

Other Useful Commands

Machine Info

# Machine serial number
$ system_profiler SPHardwareDataType | awk '/Serial/ {print $4}'

# Or using ioreg
$ ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformSerialNumber/{print $4}'

# Model identifier
$ sysctl -n hw.model

# Check warranty status (use serial with Apple's checker)

last: Login History

# Show login history
$ last
john     ttys001  192.168.1.100    Mon Jan 15 10:00 - 10:30  (00:30)
john     console                   Mon Jan 15 08:00   still logged in
reboot   ~                         Mon Jan 15 07:59

# Show reboots
$ last reboot

# Show shutdowns
$ last shutdown

uptime: System Uptime

$ uptime
10:00  up 3 days,  2:15, 2 users, load averages: 2.15 2.34 2.45

w: Who is Logged In

$ w
10:00  up 3 days,  2:15, 2 users, load averages: 2.15 2.34 2.45
USER     TTY      FROM              LOGIN@  IDLE WHAT
john     console  -                Mon08   3days -
john     s001     192.168.1.100    10:00       - w

Scripting Examples

Gather System Report

#!/bin/bash
# system_report.sh - Generate system information report

echo "=== System Report $(date) ==="
echo ""

echo "--- macOS Version ---"
sw_vers

echo ""
echo "--- Hardware ---"
system_profiler SPHardwareDataType | grep -E "Model Name|Chip|Memory|Serial"

echo ""
echo "--- Disk Usage ---"
df -h /

echo ""
echo "--- Memory ---"
vm_stat | head -10

echo ""
echo "--- Uptime ---"
uptime

echo ""
echo "--- Network ---"
ifconfig en0 | grep "inet "

Check System Requirements

#!/bin/bash
# check_requirements.sh - Verify system meets requirements

MIN_MACOS="13.0"
MIN_RAM_GB=8

# Check macOS version
MACOS_VERSION=$(sw_vers -productVersion)
if [ "$(printf '%s\n' "$MIN_MACOS" "$MACOS_VERSION" | sort -V | head -n1)" != "$MIN_MACOS" ]; then
    echo "Error: Requires macOS $MIN_MACOS or later (found $MACOS_VERSION)"
    exit 1
fi

# Check RAM
RAM_BYTES=$(sysctl -n hw.memsize)
RAM_GB=$((RAM_BYTES / 1073741824))
if [ "$RAM_GB" -lt "$MIN_RAM_GB" ]; then
    echo "Error: Requires ${MIN_RAM_GB}GB RAM (found ${RAM_GB}GB)"
    exit 1
fi

# Check architecture
ARCH=$(uname -m)
echo "System check passed: macOS $MACOS_VERSION, ${RAM_GB}GB RAM, $ARCH"

Summary: Command Reference

InformationCommand
macOS versionsw_vers -productVersion
Kernel versionuname -r
Architectureuname -m
CPU infosysctl -n machdep.cpu.brand_string
CPU coressysctl -n hw.ncpu
Memorysysctl -n hw.memsize
Modelsysctl -n hw.model
Serial numberioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/Serial/{print $4}'
Hardware detailssystem_profiler SPHardwareDataType
Disk spacedf -h
Memory statsvm_stat
Batterysystem_profiler SPPowerDataType
Uptimeuptime
Processesps aux or top
Network configifconfig or networksetup -getinfo "Wi-Fi"

Writing Portable Shell Scripts

Scripts that work perfectly on Linux often break on macOS. This chapter provides techniques for writing shell scripts that run correctly on both platforms, covering common pitfalls and their solutions.

The Challenge

A script that runs on one platform might fail on another due to:

  • Different command-line tools (GNU vs BSD)
  • Different default shells (zsh vs bash)
  • Different file system behaviors
  • Missing commands
  • Different option flags for the same command

Start with the Shebang

The first line determines which interpreter runs your script:

#!/bin/bash            # Explicit bash (best for portability)
#!/bin/sh              # POSIX shell (most portable, but limited)
#!/usr/bin/env bash    # Find bash in PATH (handles different locations)
#!/bin/zsh             # zsh (macOS default since Catalina)

Recommendations:

# Most portable - uses whatever bash is available
#!/usr/bin/env bash

# For scripts requiring bash features
#!/usr/bin/env bash
set -euo pipefail      # Strict mode

# For maximum portability (POSIX only)
#!/bin/sh
# Then use only POSIX-compliant syntax

Note: macOS ships with Bash 3.2 (from 2007) due to licensing. For Bash 4+ features, users need Homebrew’s bash:

# Check bash version
$ /bin/bash --version
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)

$ /opt/homebrew/bin/bash --version
GNU bash, version 5.2.21(1)-release (aarch64-apple-darwin23)

Detecting the Operating System

#!/usr/bin/env bash

# Method 1: uname
OS=$(uname -s)
case "$OS" in
    Linux*)  PLATFORM="Linux";;
    Darwin*) PLATFORM="macOS";;
    CYGWIN*) PLATFORM="Cygwin";;
    MINGW*)  PLATFORM="MinGW";;
    *)       PLATFORM="Unknown";;
esac

# Method 2: Check for specific files
if [ -f /etc/os-release ]; then
    # Linux (most distributions)
    . /etc/os-release
    echo "Linux: $NAME $VERSION"
elif [ -f /System/Library/CoreServices/SystemVersion.plist ]; then
    # macOS
    echo "macOS: $(sw_vers -productVersion)"
fi

# Method 3: OSTYPE variable (bash)
case "$OSTYPE" in
    darwin*)  echo "macOS" ;;
    linux*)   echo "Linux" ;;
    bsd*)     echo "BSD" ;;
    msys*)    echo "Windows/MSYS" ;;
    *)        echo "Unknown: $OSTYPE" ;;
esac

Handling sed Differences

The -i flag is the most common issue:

# WRONG - GNU syntax fails on macOS
sed -i 's/old/new/g' file.txt

# PORTABLE SOLUTION 1: Use a function
sed_i() {
    if [[ "$OSTYPE" == darwin* ]]; then
        sed -i '' "$@"
    else
        sed -i "$@"
    fi
}

# Usage
sed_i 's/old/new/g' file.txt

# PORTABLE SOLUTION 2: Use a temp file (most portable)
sed 's/old/new/g' file.txt > file.tmp && mv file.tmp file.txt

# PORTABLE SOLUTION 3: Use perl (if available)
perl -i -pe 's/old/new/g' file.txt

More sed portability:

# Newlines - GNU sed understands \n, BSD doesn't
# Portable: use a literal newline or $'\n'
sed 's/$/\
/' file.txt

# Or use printf
nl=$'\n'
sed "s/$/$nl/" file.txt

# Extended regex - use -E on both (works on modern BSD and GNU)
sed -E 's/[0-9]+/NUMBER/g' file.txt

Handling grep Differences

# WRONG - Perl regex not available on BSD
grep -P '\d+' file.txt

# PORTABLE - Use extended regex with POSIX character classes
grep -E '[0-9]+' file.txt

# Word boundaries
# GNU: \bword\b
# BSD: [[:<:]]word[[:>:]]
# PORTABLE: Use -w flag
grep -w 'word' file.txt

# Portable function for patterns
grep_digits() {
    grep -E '[0-9]+' "$@"
}

Handling date Differences

Date parsing differs dramatically:

# Get date N days ago
get_date_ago() {
    local days=$1
    if [[ "$OSTYPE" == darwin* ]]; then
        date -v-${days}d +%Y-%m-%d
    else
        date -d "$days days ago" +%Y-%m-%d
    fi
}

# Parse a date string
parse_date() {
    local datestr=$1
    local format=$2
    if [[ "$OSTYPE" == darwin* ]]; then
        date -j -f "$format" "$datestr" +%s
    else
        date -d "$datestr" +%s
    fi
}

# Get current timestamp (portable)
date +%s

# Format current date (portable)
date +%Y-%m-%d
date +"%Y-%m-%d %H:%M:%S"

Handling stat Differences

# Get file size
get_file_size() {
    local file=$1
    if [[ "$OSTYPE" == darwin* ]]; then
        stat -f%z "$file"
    else
        stat -c%s "$file"
    fi
}

# Get modification time
get_mtime() {
    local file=$1
    if [[ "$OSTYPE" == darwin* ]]; then
        stat -f%m "$file"
    else
        stat -c%Y "$file"
    fi
}

# MOST PORTABLE: Use wc or ls
file_size=$(wc -c < "$file")
# or
file_size=$(ls -l "$file" | awk '{print $5}')
# Get canonical path
get_realpath() {
    local path=$1
    if command -v realpath &> /dev/null; then
        realpath "$path"
    elif command -v greadlink &> /dev/null; then
        greadlink -f "$path"
    elif [[ "$OSTYPE" == darwin* ]]; then
        # macOS without coreutils
        python3 -c "import os; print(os.path.realpath('$path'))"
    else
        readlink -f "$path"
    fi
}

# Or use this POSIX-compliant function
realpath_posix() {
    local path=$1
    if [ -d "$path" ]; then
        (cd "$path" && pwd -P)
    else
        (cd "$(dirname "$path")" && echo "$(pwd -P)/$(basename "$path")")
    fi
}

Handling mktemp Differences

# PORTABLE: Always use a template
tmpfile=$(mktemp /tmp/myscript.XXXXXX)
tmpdir=$(mktemp -d /tmp/myscript.XXXXXX)

# Cleanup on exit
cleanup() {
    rm -rf "$tmpfile" "$tmpdir"
}
trap cleanup EXIT

Handling Array Differences

Bash arrays work the same, but be aware of version differences:

# Bash 3.2 (macOS default) vs Bash 4+
# Associative arrays require Bash 4+
# declare -A map  # Fails on macOS default bash

# PORTABLE: Check bash version
if ((BASH_VERSINFO[0] >= 4)); then
    declare -A map
    map[key]="value"
else
    # Use a different approach
    echo "Warning: Associative arrays not supported, using files"
fi

# Regular arrays work on both
arr=("one" "two" "three")
for item in "${arr[@]}"; do
    echo "$item"
done

Finding Commands

# Check if command exists
command_exists() {
    command -v "$1" &> /dev/null
}

# Find preferred command
find_editor() {
    for editor in nvim vim vi nano; do
        if command_exists "$editor"; then
            echo "$editor"
            return
        fi
    done
    echo "cat"  # Fallback
}

# Use GNU tool if available, fall back to BSD
SED=$(command -v gsed || command -v sed)
GREP=$(command -v ggrep || command -v grep)
DATE=$(command -v gdate || command -v date)

Clipboard Operations

# Copy to clipboard
copy_to_clipboard() {
    if [[ "$OSTYPE" == darwin* ]]; then
        pbcopy
    elif command_exists xclip; then
        xclip -selection clipboard
    elif command_exists xsel; then
        xsel --clipboard --input
    else
        echo "No clipboard tool available" >&2
        return 1
    fi
}

# Paste from clipboard
paste_from_clipboard() {
    if [[ "$OSTYPE" == darwin* ]]; then
        pbpaste
    elif command_exists xclip; then
        xclip -selection clipboard -o
    elif command_exists xsel; then
        xsel --clipboard --output
    else
        echo "No clipboard tool available" >&2
        return 1
    fi
}

# Usage
echo "Hello" | copy_to_clipboard
paste_from_clipboard

Opening Files and URLs

# Open file with default application
open_file() {
    if [[ "$OSTYPE" == darwin* ]]; then
        open "$@"
    elif command_exists xdg-open; then
        xdg-open "$@"
    else
        echo "No 'open' command available" >&2
        return 1
    fi
}

# Open URL in browser
open_url() {
    local url=$1
    if [[ "$OSTYPE" == darwin* ]]; then
        open "$url"
    elif command_exists xdg-open; then
        xdg-open "$url"
    elif command_exists sensible-browser; then
        sensible-browser "$url"
    else
        echo "Cannot open URL: $url" >&2
        return 1
    fi
}

Network Operations

# Get primary IP address
get_ip() {
    if [[ "$OSTYPE" == darwin* ]]; then
        ipconfig getifaddr en0 2>/dev/null || \
        ipconfig getifaddr en1 2>/dev/null
    else
        hostname -I | awk '{print $1}'
    fi
}

# Check if port is open
check_port() {
    local host=$1
    local port=$2
    if command_exists nc; then
        nc -zv "$host" "$port" 2>&1
    elif command_exists timeout; then
        timeout 1 bash -c "echo > /dev/tcp/$host/$port" 2>/dev/null
    else
        (echo > /dev/tcp/"$host"/"$port") 2>/dev/null
    fi
}

Process Management

# Get process ID by name
get_pid() {
    local name=$1
    if [[ "$OSTYPE" == darwin* ]]; then
        pgrep -f "$name"
    else
        pgrep -f "$name"
    fi
}

# Kill process by name
kill_by_name() {
    local name=$1
    if [[ "$OSTYPE" == darwin* ]]; then
        pkill -f "$name"
    else
        pkill -f "$name"
    fi
}

# Check if process is running
is_running() {
    local name=$1
    pgrep -f "$name" > /dev/null 2>&1
}

Complete Portable Script Template

#!/usr/bin/env bash
#
# script_name.sh - Description of what this script does
#
# Usage: script_name.sh [options] arguments
#

set -euo pipefail

# ============================================================================
# Configuration
# ============================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_NAME="$(basename "$0")"

# Detect OS
case "$OSTYPE" in
    darwin*)  OS="macos" ;;
    linux*)   OS="linux" ;;
    *)        OS="unknown" ;;
esac

# ============================================================================
# Utility Functions
# ============================================================================

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}

error() {
    log "ERROR: $*"
    exit 1
}

command_exists() {
    command -v "$1" &> /dev/null
}

require_command() {
    if ! command_exists "$1"; then
        error "Required command not found: $1"
    fi
}

# ============================================================================
# OS-Specific Functions
# ============================================================================

# Portable sed in-place
sed_i() {
    if [[ "$OS" == "macos" ]]; then
        sed -i '' "$@"
    else
        sed -i "$@"
    fi
}

# Portable date manipulation
date_ago() {
    local days=$1
    if [[ "$OS" == "macos" ]]; then
        date -v-${days}d +%Y-%m-%d
    else
        date -d "$days days ago" +%Y-%m-%d
    fi
}

# Portable file size
file_size() {
    wc -c < "$1" | tr -d ' '
}

# Portable realpath
get_realpath() {
    if command_exists realpath; then
        realpath "$1"
    elif [[ "$OS" == "macos" ]] && command_exists greadlink; then
        greadlink -f "$1"
    else
        (cd "$(dirname "$1")" && echo "$(pwd)/$(basename "$1")")
    fi
}

# ============================================================================
# Main Script
# ============================================================================

usage() {
    cat << EOF
Usage: $SCRIPT_NAME [options] <arguments>

Description of what this script does.

Options:
    -h, --help      Show this help message
    -v, --verbose   Enable verbose output
    -d, --debug     Enable debug mode

Examples:
    $SCRIPT_NAME file.txt
    $SCRIPT_NAME -v directory/

EOF
}

main() {
    local verbose=false
    local debug=false

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help)
                usage
                exit 0
                ;;
            -v|--verbose)
                verbose=true
                shift
                ;;
            -d|--debug)
                debug=true
                set -x
                shift
                ;;
            --)
                shift
                break
                ;;
            -*)
                error "Unknown option: $1"
                ;;
            *)
                break
                ;;
        esac
    done

    # Validate arguments
    if [[ $# -lt 1 ]]; then
        usage
        error "Missing required argument"
    fi

    # Check dependencies
    require_command sed
    require_command grep

    # Main logic here
    log "Running on $OS"
    log "Processing: $*"

    # Your code here...
}

# Run main function
main "$@"

Testing Portability

Test with Docker

# Test on Linux
docker run --rm -v "$PWD:/work" -w /work alpine:latest sh ./script.sh
docker run --rm -v "$PWD:/work" -w /work ubuntu:latest bash ./script.sh

# Test on different shells
docker run --rm -v "$PWD:/work" -w /work bash:5.2 bash ./script.sh
docker run --rm -v "$PWD:/work" -w /work bash:3.2 bash ./script.sh

Use shellcheck

# Install shellcheck
brew install shellcheck

# Check your script
shellcheck script.sh

# Fix common issues
shellcheck -f diff script.sh | patch -p1

Use shfmt

# Install shfmt
brew install shfmt

# Format script
shfmt -w script.sh

# Check for issues
shfmt -d script.sh

Summary: Portability Checklist

Before distributing a script:

  1. Shebang: Use #!/usr/bin/env bash or #!/bin/sh for POSIX
  2. sed -i: Use temp files or detect OS
  3. grep: Avoid -P, use -E with POSIX classes
  4. date: Abstract into functions
  5. stat: Use wc -c or ls for file size
  6. readlink -f: Provide fallback function
  7. Arrays: Avoid associative arrays for Bash 3.2 compatibility
  8. Test: Test on both macOS and Linux
  9. Lint: Run shellcheck

Common compatibility functions:

TaskPortable Approach
In-place sedTemp file or OS detection
File sizewc -c < file
Canonical pathCustom function
Date mathOS-specific functions
ClipboardOS detection (pbcopy vs xclip)
Open fileOS detection (open vs xdg-open)
Command checkcommand -v cmd

Development on macOS

macOS is a powerful development platform that combines the familiarity of Unix with Apple’s unique tooling and conventions. For developers coming from Linux or other Unix systems, macOS presents a familiar environment with important differences that affect how you compile code, link libraries, debug programs, and build for multiple architectures.

This part covers the practical aspects of developing software on macOS, from installing the toolchain to profiling production code.

The macOS Development Stack

The development environment on macOS consists of several layers:

┌─────────────────────────────────────────┐
│     Your Application / Project          │
├─────────────────────────────────────────┤
│  Frameworks (Cocoa, Foundation, etc.)   │
├─────────────────────────────────────────┤
│  Libraries (dylib, tbd, static)         │
├─────────────────────────────────────────┤
│  Toolchain (Clang/LLVM, linker, tools)  │
├─────────────────────────────────────────┤
│  Xcode Command Line Tools / Xcode       │
├─────────────────────────────────────────┤
│  Darwin / macOS                         │
└─────────────────────────────────────────┘

Key Differences from Linux Development

AspectLinuxmacOS
CompilerGCC (usually)Clang/LLVM (always)
Default shellBashZsh
Shared libraries.so files.dylib files
Library pathLD_LIBRARY_PATHDYLD_LIBRARY_PATH
Binary inspectionldd, readelfotool, nm
DebuggerGDBLLDB
Package formatELFMach-O
FrameworksN/ANative concept
Multiple architecturesSeparate binariesUniversal binaries

What You’ll Learn in This Part

Xcode Command Line Tools covers installing and managing the essential development tools, including what’s included and how to switch between full Xcode and standalone tools.

The Clang/LLVM Toolchain explains how macOS uses Clang as its compiler, why gcc is actually Clang, and the practical differences from GCC.

Compiling Software from Source walks through building open-source software on macOS, including common pitfalls and solutions.

Library Paths and Linking demystifies dynamic linking on macOS, covering dylib files, install names, rpath, and the tools to inspect and modify them.

Frameworks vs Unix Libraries explains Apple’s framework concept, how frameworks differ from traditional Unix libraries, and when to use each.

Debugging with LLDB provides a practical guide to macOS’s native debugger, with command mappings for GDB users.

Performance Profiling Tools covers Instruments, DTrace, sample, and other tools for understanding program behavior.

Universal Binaries and Architecture explains fat binaries, the transition to Apple Silicon, and how to build for multiple architectures.

Prerequisites

Before diving into macOS development, you should:

  1. Have Terminal.app or another terminal emulator ready
  2. Be comfortable with basic command-line operations
  3. Understand fundamental compilation concepts (source, object files, linking)

A Note on Apple Silicon

The transition from Intel to Apple Silicon affects many aspects of development:

  • Universal binaries contain code for both architectures
  • Rosetta 2 can run Intel binaries on Apple Silicon
  • Different Homebrew installation paths (/opt/homebrew vs /usr/local)
  • Some tools behave differently depending on architecture

Throughout this part, we’ll note where architecture matters and how to handle both platforms.

Xcode Command Line Tools

The Xcode Command Line Tools package provides essential development utilities for macOS. You don’t need the full Xcode IDE to compile code, run scripts, or use Unix development tools. The standalone Command Line Tools package gives you compilers, linkers, headers, and utilities in a much smaller download.

What’s Included

The Command Line Tools package contains:

Compilers and Build Tools

ToolDescription
clangC, C++, Objective-C compiler
clang++C++ compiler (same as clang with C++ mode)
swiftSwift compiler
ldThe linker
asAssembler
arArchive tool (static libraries)
libtoolLibrary creation tool
makeBuild automation
cmakeCross-platform build system (recent versions)

Development Utilities

ToolDescription
gitVersion control
svnSubversion (legacy)
lldbDebugger
nmSymbol table viewer
otoolObject file viewer
lipoUniversal binary tool
codesignCode signing
install_name_toolLibrary path modifier
dsymutilDebug symbol utility
dwarfdumpDWARF debug info viewer
stripRemove symbols from binaries
sizeDisplay binary section sizes

Headers and SDKs

The tools include macOS SDK headers necessary for compiling programs:

# SDK location
$ xcrun --show-sdk-path
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk

# SDK version
$ xcrun --show-sdk-version
14.4

Other Utilities

ToolDescription
xcrunRun tools from active developer directory
xcode-selectManage developer tool paths
xcodebuildBuild Xcode projects (limited without full Xcode)
instrumentsCommand-line profiling (requires Xcode)

Installing Command Line Tools

Interactive Installation

The simplest method triggers an installation dialog:

$ xcode-select --install

This opens a dialog offering to install the Command Line Tools. Click “Install” and wait for the download (approximately 1-2 GB).

Trigger via Missing Tool

Running any development command without installed tools triggers the prompt:

$ git --version
xcode-select: note: no developer tools were found at '/Applications/Xcode.app'
# Installation dialog appears

Download from Apple Developer

For specific versions or offline installation:

  1. Visit developer.apple.com/download/more/
  2. Sign in with Apple ID (free account works)
  3. Search for “Command Line Tools”
  4. Download the .dmg for your macOS version
  5. Install the package

Verify Installation

$ xcode-select -p
/Library/Developer/CommandLineTools

$ clang --version
Apple clang version 15.0.0 (clang-1500.3.9.4)
Target: arm64-apple-darwin23.4.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

$ git --version
git version 2.39.3 (Apple Git-146)

Xcode vs Command Line Tools

You can develop on macOS with either:

  1. Command Line Tools only (standalone)
  2. Full Xcode (includes Command Line Tools)

Command Line Tools Only

Pros:

  • Smaller download (~2 GB vs ~12+ GB)
  • No IDE required
  • Sufficient for most Unix development
  • Faster installation

Cons:

  • No iOS/watchOS/tvOS development
  • No Interface Builder
  • Limited Instruments profiling
  • No Simulator
  • No Xcode IDE features

Full Xcode

Required for:

  • iOS, iPadOS, watchOS, tvOS, visionOS development
  • SwiftUI previews
  • Full Instruments profiling
  • Metal shader development
  • App Store submission
  • Simulator usage

Installation Locations

# Command Line Tools (standalone)
/Library/Developer/CommandLineTools/
├── Library/
│   └── Frameworks/         # Development frameworks
├── SDKs/
│   └── MacOSX.sdk         # macOS SDK
└── usr/
    ├── bin/               # Tools (clang, git, etc.)
    ├── include/           # C headers
    └── lib/               # Libraries

# Xcode (full)
/Applications/Xcode.app/
└── Contents/
    └── Developer/
        ├── Platforms/     # iOS, macOS, etc.
        ├── SDKs/
        ├── Toolchains/
        └── usr/
            └── bin/       # Tools

Switching Between Xcode and Command Line Tools

The xcode-select command controls which developer directory is active:

Check Current Selection

$ xcode-select -p
/Applications/Xcode.app/Contents/Developer
# or
/Library/Developer/CommandLineTools

Switch to Xcode

$ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

Switch to Command Line Tools

$ sudo xcode-select -s /Library/Developer/CommandLineTools

Reset to Default

$ sudo xcode-select -r

Why Switch?

Common reasons to switch:

ScenarioUse
Building iOS appsXcode
Homebrew formula developmentCommand Line Tools (often)
Compiling Unix softwareEither works
Using different SDK versionSwitch as needed
CI/CD serverCommand Line Tools (smaller)

The xcrun Command

xcrun executes tools from the active developer directory, ensuring you use the correct version:

# Run clang from current developer directory
$ xcrun clang -v

# Find tool location
$ xcrun -f clang
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang

# Show SDK path
$ xcrun --show-sdk-path
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.4.sdk

# Run with specific SDK
$ xcrun --sdk macosx clang -v

# List available SDKs
$ xcodebuild -showsdks
macOS SDKs:
        macOS 14.4                      -sdk macosx14.4

iOS SDKs:
        iOS 17.4                        -sdk iphoneos17.4
...

Using xcrun in Scripts

For portable scripts that work with either Xcode or Command Line Tools:

#!/bin/bash
# Use xcrun to find compiler
CC=$(xcrun -f clang)
SDK=$(xcrun --show-sdk-path)

$CC -isysroot "$SDK" -o myprogram myprogram.c

SDK Management

Listing Installed SDKs

# With Xcode
$ ls /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/
MacOSX.sdk  MacOSX14.4.sdk

# With Command Line Tools
$ ls /Library/Developer/CommandLineTools/SDKs/
MacOSX.sdk  MacOSX14.4.sdk

SDK Contents

$ ls $(xcrun --show-sdk-path)
Entitlements.plist  SDKSettings.json  System/
Library/            SDKSettings.plist usr/

The SDK contains:

  • System/: System framework stubs
  • usr/: Headers and libraries
  • Library/: Frameworks

Targeting Specific SDK Versions

# Compile against specific SDK
$ clang -isysroot $(xcrun --show-sdk-path --sdk macosx14.4) program.c

# Set deployment target (minimum OS version)
$ clang -mmacosx-version-min=12.0 program.c

Updating Command Line Tools

Check for Updates

$ softwareupdate --list
Software Update found the following new or updated software:
* Command Line Tools for Xcode-15.3

Install Updates

$ softwareupdate --install "Command Line Tools for Xcode-15.3"

Reinstall After macOS Upgrade

After major macOS upgrades, you may need to reinstall:

# Remove existing tools
$ sudo rm -rf /Library/Developer/CommandLineTools

# Reinstall
$ xcode-select --install

Troubleshooting

“No Developer Tools Found”

$ git status
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools)

Solution:

$ xcode-select --install

“Agreeing to Xcode License”

$ clang
Agreeing to the Xcode/iOS license requires admin privileges, please run "sudo xcodebuild -license"

Solution:

$ sudo xcodebuild -license accept

Wrong SDK or Tools Version

# Check what's active
$ xcode-select -p
$ xcrun --show-sdk-version

# Switch if needed
$ sudo xcode-select -s /path/to/correct/developer/directory

Headers Not Found After macOS Update

$ clang program.c
fatal error: 'stdio.h' file not found

Reinstall Command Line Tools:

$ sudo rm -rf /Library/Developer/CommandLineTools
$ xcode-select --install

Multiple Xcode Versions

You can have multiple Xcode versions installed:

# List installations
$ ls /Applications/ | grep Xcode
Xcode.app
Xcode-15.2.app

# Switch between them
$ sudo xcode-select -s /Applications/Xcode-15.2.app/Contents/Developer

Command Line Tools Components

Examining Package Contents

# List package contents
$ pkgutil --files com.apple.pkg.CLTools_Executables | head -20
Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework
Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Headers
Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Python3
...

Package Receipts

# List installed developer packages
$ pkgutil --pkgs | grep -i cltools
com.apple.pkg.CLTools_Executables
com.apple.pkg.CLTools_SDK_macOS14
com.apple.pkg.CLTools_SwiftBackDeploy
com.apple.pkg.CLTools_macOS_SDK

Summary

TaskCommand
Install CLI toolsxcode-select --install
Check installation pathxcode-select -p
Switch to Xcodesudo xcode-select -s /Applications/Xcode.app/Contents/Developer
Switch to CLI toolssudo xcode-select -s /Library/Developer/CommandLineTools
Reset to defaultsudo xcode-select -r
Find tool pathxcrun -f <tool>
Get SDK pathxcrun --show-sdk-path
Run tool with SDKxcrun --sdk macosx <command>

The Command Line Tools package provides everything needed for Unix-style development on macOS without the full Xcode installation. For most developers who don’t need iOS development or the Xcode IDE, it’s the right choice.

The Clang/LLVM Toolchain

macOS uses Clang as its system compiler. This is a significant difference from most Linux distributions, which typically default to GCC. Understanding the Clang/LLVM toolchain helps you write portable code and take advantage of macOS-specific optimizations.

gcc Is Really Clang

On macOS, running gcc actually invokes Clang:

$ gcc --version
Apple clang version 15.0.0 (clang-1500.3.9.4)
Target: arm64-apple-darwin23.4.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

$ which gcc
/usr/bin/gcc

$ ls -la /usr/bin/gcc
-rwxr-xr-x  1 root  wheel  167120 Feb 20 18:00 /usr/bin/gcc

Despite the name, /usr/bin/gcc is Apple’s Clang. This shim exists for compatibility with build systems that expect gcc.

Verifying Clang Is In Use

# All these commands run the same compiler
$ gcc --version 2>&1 | head -1
Apple clang version 15.0.0 (clang-1500.3.9.4)

$ clang --version 2>&1 | head -1
Apple clang version 15.0.0 (clang-1500.3.9.4)

$ cc --version 2>&1 | head -1
Apple clang version 15.0.0 (clang-1500.3.9.4)

Apple Clang vs Upstream LLVM

Apple ships its own Clang fork with modifications:

# Apple Clang version
$ clang --version
Apple clang version 15.0.0 (clang-1500.3.9.4)

# Upstream LLVM Clang (if installed via Homebrew)
$ /opt/homebrew/opt/llvm/bin/clang --version
clang version 17.0.6
Target: arm64-apple-darwin23.4.0
Thread model: posix
InstalledDir: /opt/homebrew/opt/llvm/bin

Apple Clang differences:

  • Version numbers don’t match upstream LLVM
  • Includes Apple-specific features and optimizations
  • May lag behind upstream in some features
  • Better integration with macOS SDKs and frameworks

Clang vs GCC Differences

Command-Line Compatibility

Most GCC options work with Clang:

# These work the same
$ clang -O2 -Wall -o program program.c
$ gcc -O2 -Wall -o program program.c     # Really calls clang

Key Differences

FeatureGCCClang
Default standard-std=gnu17-std=gnu17
Warning flagsGCC-specific availableMostly compatible
Error messagesGoodExcellent (clearer)
ExtensionsGCC extensionsGCC + Clang extensions
Static analysis-fanalyzer--analyze
SanitizersAvailableBetter integration
ModulesLimitedBetter C++20 modules

GCC-Specific Features Not in Clang

Some GCC flags don’t exist in Clang:

# GCC-only flag
$ clang -fno-semantic-interposition program.c
clang: warning: argument unused during compilation: '-fno-semantic-interposition'

# GCC's link-time optimization flag
$ clang -flto=auto program.c
error: invalid argument 'auto' to -flto

# Clang equivalent
$ clang -flto=thin program.c   # or -flto=full

Clang-Specific Features

# Clang's excellent error messages
$ cat error.c
int main() {
    int x = "hello";
    return 0;
}

$ clang error.c
error.c:2:9: error: incompatible pointer to integer conversion initializing 'int' with an expression of type 'char[6]' [-Wint-conversion]
    int x = "hello";
        ^   ~~~~~~~

# Clang static analyzer
$ clang --analyze program.c
program.c:15:5: warning: Use of memory after it is freed
    return *ptr;
           ^~~~

Compiler Flags Reference

Optimization Levels

# No optimization (debugging)
$ clang -O0 -g program.c

# Basic optimization
$ clang -O1 program.c

# Standard optimization
$ clang -O2 program.c

# Aggressive optimization
$ clang -O3 program.c

# Optimize for size
$ clang -Os program.c

# Optimize for size, more aggressive
$ clang -Oz program.c

# Link-time optimization (can catch more issues)
$ clang -flto program.c

Warning Flags

# Common warning set
$ clang -Wall -Wextra program.c

# All warnings Clang offers
$ clang -Weverything program.c  # Usually too noisy

# Treat warnings as errors
$ clang -Werror program.c

# Specific warnings
$ clang -Wconversion -Wshadow -Wformat=2 program.c

# Disable specific warning
$ clang -Wno-unused-variable program.c

Useful Warning Categories

FlagDescription
-WallCommon warnings
-WextraAdditional warnings
-WpedanticStrict ISO compliance
-WconversionImplicit conversions
-WshadowVariable shadowing
-Wformat=2Format string issues
-Wnull-dereferenceNull pointer dereference
-WuninitializedUninitialized variables

Debug Information

# Standard debug info
$ clang -g program.c

# Debug info + optimization (may confuse debugger)
$ clang -g -O2 program.c

# Include macro definitions in debug info
$ clang -g3 program.c

# DWARF version
$ clang -gdwarf-4 program.c

Architecture and Target

# Build for specific architecture
$ clang -arch arm64 program.c
$ clang -arch x86_64 program.c

# Universal binary (both architectures)
$ clang -arch arm64 -arch x86_64 program.c

# Target triple (more explicit)
$ clang --target=arm64-apple-macos13 program.c

# Minimum macOS version
$ clang -mmacosx-version-min=12.0 program.c

Language Standards

# C standards
$ clang -std=c99 program.c
$ clang -std=c11 program.c
$ clang -std=c17 program.c  # Default
$ clang -std=c23 program.c

# C++ standards
$ clang++ -std=c++11 program.cpp
$ clang++ -std=c++14 program.cpp
$ clang++ -std=c++17 program.cpp
$ clang++ -std=c++20 program.cpp
$ clang++ -std=c++23 program.cpp

# GNU extensions (default)
$ clang -std=gnu17 program.c
$ clang++ -std=gnu++20 program.cpp

Sanitizers

Clang’s sanitizers help find bugs at runtime:

Address Sanitizer (ASan)

Detects memory errors:

$ clang -fsanitize=address -g program.c -o program
$ ./program
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
    #0 0x... in main program.c:10

Detects:

  • Buffer overflows (stack, heap, global)
  • Use after free
  • Double free
  • Memory leaks (with -fsanitize=address,leak)

Undefined Behavior Sanitizer (UBSan)

$ clang -fsanitize=undefined -g program.c -o program
$ ./program
program.c:5:15: runtime error: signed integer overflow: 2147483647 + 1

Detects:

  • Integer overflow
  • Null pointer dereference
  • Division by zero
  • Invalid shifts

Thread Sanitizer (TSan)

$ clang -fsanitize=thread -g program.c -o program -lpthread
$ ./program
WARNING: ThreadSanitizer: data race (pid=12345)
  Write of size 4 at 0x... by thread T1:

Memory Sanitizer (MSan)

Note: Not available in Apple Clang. Use Homebrew LLVM:

$ /opt/homebrew/opt/llvm/bin/clang -fsanitize=memory -g program.c

Combining Sanitizers

# Address + Undefined behavior
$ clang -fsanitize=address,undefined -g program.c

# Note: Thread sanitizer cannot combine with Address sanitizer

Static Analysis

Built-in Analyzer

# Run static analyzer
$ clang --analyze program.c

# Verbose output
$ clang --analyze -Xanalyzer -analyzer-output=text program.c

# Generate HTML report
$ clang --analyze -Xanalyzer -analyzer-output=html -o report/ program.c

scan-build Wrapper

# Analyze entire build
$ scan-build make

# With specific compiler
$ scan-build --use-cc=clang make

Preprocessor

Viewing Preprocessor Output

# Output preprocessed code
$ clang -E program.c > program.i

# With line markers
$ clang -E program.c

# Without line markers
$ clang -E -P program.c

Predefined Macros

# List all predefined macros
$ clang -dM -E - < /dev/null

# Apple-specific macros
$ clang -dM -E - < /dev/null | grep -i apple
#define __APPLE__ 1
#define __APPLE_CC__ 6000

# Architecture macros
$ clang -dM -E - < /dev/null | grep -E "__(arm|x86|aarch)"
#define __aarch64__ 1
#define __arm64__ 1

Common macOS Macros

MacroDescription
__APPLE__Always defined on Apple platforms
__MACH__Mach kernel (macOS, iOS)
TARGET_OS_MACmacOS (from TargetConditionals.h)
__arm64__Apple Silicon
__x86_64__Intel 64-bit

Conditional Compilation

#ifdef __APPLE__
    #include <TargetConditionals.h>
    #if TARGET_OS_MAC
        // macOS-specific code
    #endif
#endif

#if defined(__arm64__)
    // Apple Silicon code
#elif defined(__x86_64__)
    // Intel code
#endif

Installing Real GCC

If you need actual GCC (not Apple’s Clang wrapper):

# Install via Homebrew
$ brew install gcc

# This installs as gcc-14 (or current version)
$ gcc-14 --version
gcc-14 (Homebrew GCC 14.1.0) 14.1.0

# Create alias if needed
$ alias gcc='gcc-14'
$ alias g++='g++-14'

Why Use Real GCC?

  • Compatibility testing with Linux builds
  • GCC-specific extensions or optimizations
  • Different error/warning messages
  • OpenMP support differences
  • Fortran support (gfortran)
# Install Fortran compiler
$ brew install gcc
$ gfortran-14 --version
GNU Fortran (Homebrew GCC 14.1.0) 14.1.0

Clang Tools

The LLVM project includes additional tools:

clang-format

# Format code
$ clang-format -i program.c

# With style
$ clang-format --style=LLVM -i program.c
$ clang-format --style=Google -i program.c

# Create style file
$ clang-format --style=LLVM --dump-config > .clang-format

clang-tidy

# Install via Homebrew (not in Apple's tools)
$ brew install llvm

# Run linter
$ /opt/homebrew/opt/llvm/bin/clang-tidy program.c -- -I/path/to/includes

# With checks
$ clang-tidy -checks='modernize-*,readability-*' program.cpp

Compilation Database

Many tools use compilation databases:

# Generate with CMake
$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..

# Generate with Bear (for make-based projects)
$ brew install bear
$ bear -- make

# Creates compile_commands.json
$ cat compile_commands.json
[
  {
    "directory": "/path/to/project",
    "command": "clang -c -o program.o program.c",
    "file": "program.c"
  }
]

Summary

Key points about Clang on macOS:

AspectDetail
gcc commandRuns Apple Clang, not GCC
Apple ClangModified LLVM with Apple extensions
CompatibilityMost GCC flags work
AdvantagesBetter errors, sanitizers, static analysis
Real GCCAvailable via Homebrew as gcc-14
SanitizersASan, UBSan, TSan available
Static analysisclang --analyze

Understanding that macOS uses Clang helps you write portable code and take advantage of Clang’s excellent diagnostics and analysis tools.

Compiling Software from Source

Compiling software from source on macOS follows familiar Unix patterns but with important differences. This chapter covers the practical aspects of building open-source software, common pitfalls, and macOS-specific solutions.

Basic Build Process

Most Unix software follows the configure-make-install pattern:

# Download and extract
$ curl -LO https://example.com/software-1.0.tar.gz
$ tar xzf software-1.0.tar.gz
$ cd software-1.0

# Configure, build, install
$ ./configure --prefix=/usr/local
$ make
$ sudo make install

On macOS, this often requires adjustments.

Prerequisites

Install Command Line Tools

$ xcode-select --install

Install Common Build Dependencies

# Essential build tools
$ brew install autoconf automake libtool pkg-config cmake

# Common libraries
$ brew install openssl readline zlib

The Configure Step

Basic Configure Usage

# Show available options
$ ./configure --help

# Common options
$ ./configure \
    --prefix=/usr/local \
    --with-ssl=/opt/homebrew/opt/openssl@3 \
    --disable-static \
    --enable-shared

Important Configure Options

OptionPurpose
--prefix=PATHInstallation directory
--with-PACKAGE=PATHPath to dependency
--without-PACKAGEDisable optional feature
--enable-FEATUREEnable optional feature
--disable-FEATUREDisable feature
--build=TRIPLEBuild system type
--host=TRIPLETarget system type

macOS-Specific Configure Issues

Finding Homebrew Libraries

Configure scripts often can’t find Homebrew-installed libraries:

# Problem
$ ./configure
checking for openssl... no
configure: error: OpenSSL not found

# Solution: Tell configure where to look
$ ./configure \
    --with-openssl=/opt/homebrew/opt/openssl@3 \
    LDFLAGS="-L/opt/homebrew/opt/openssl@3/lib" \
    CPPFLAGS="-I/opt/homebrew/opt/openssl@3/include"

Using pkg-config

# Set PKG_CONFIG_PATH for Homebrew packages
$ export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH"

# Verify package is found
$ pkg-config --modversion openssl
3.2.1

# Use in configure
$ ./configure PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig"

Setting Environment Variables

# Comprehensive approach for Homebrew
$ export LDFLAGS="-L/opt/homebrew/lib"
$ export CPPFLAGS="-I/opt/homebrew/include"
$ export CFLAGS="-I/opt/homebrew/include"
$ export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig"

$ ./configure --prefix=/usr/local

Apple Silicon Specifics

# Verify architecture
$ uname -m
arm64

# Build for native architecture
$ ./configure --build=aarch64-apple-darwin

# Force x86_64 (via Rosetta)
$ arch -x86_64 ./configure --build=x86_64-apple-darwin

CMake Projects

Many modern projects use CMake instead of autoconf:

# Basic CMake build
$ mkdir build && cd build
$ cmake ..
$ make
$ sudo make install

# With options
$ cmake .. \
    -DCMAKE_INSTALL_PREFIX=/usr/local \
    -DCMAKE_BUILD_TYPE=Release \
    -DOPENSSL_ROOT_DIR=/opt/homebrew/opt/openssl@3

# Using Ninja (faster)
$ brew install ninja
$ cmake -G Ninja ..
$ ninja
$ sudo ninja install

CMake on macOS

# Tell CMake about Homebrew
$ cmake .. \
    -DCMAKE_PREFIX_PATH="/opt/homebrew" \
    -DCMAKE_INSTALL_PREFIX="/usr/local"

# Build universal binary
$ cmake .. \
    -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"

# Target specific macOS version
$ cmake .. \
    -DCMAKE_OSX_DEPLOYMENT_TARGET="12.0"

Meson Projects

# Install Meson
$ brew install meson ninja

# Basic build
$ meson setup build
$ cd build
$ ninja
$ sudo ninja install

# With options
$ meson setup build \
    --prefix=/usr/local \
    -Doption=value

Common Build Errors and Solutions

Missing Header Files

# Error
fatal error: 'openssl/ssl.h' file not found

# Solution: Add include path
$ CPPFLAGS="-I/opt/homebrew/include" ./configure
# or
$ ./configure CPPFLAGS="-I/opt/homebrew/opt/openssl@3/include"

Library Not Found

# Error
ld: library not found for -lssl

# Solution: Add library path
$ LDFLAGS="-L/opt/homebrew/lib" ./configure
# or
$ ./configure LDFLAGS="-L/opt/homebrew/opt/openssl@3/lib"

SDK Headers Not Found

After macOS upgrade, SDK headers may be missing:

# Error
fatal error: 'stdio.h' file not found

# Solution: Reinstall Command Line Tools
$ sudo rm -rf /Library/Developer/CommandLineTools
$ xcode-select --install

Incompatible Function Signatures

macOS uses BSD libc, which differs from GNU libc:

// Linux (GNU)
char *strsignal(int sig);

// macOS (BSD)
const char *strsignal(int sig);  // Note: const return

Fix: Check for macOS in code or use compatibility shims.

Missing GNU Tools

Some software expects GNU-specific tool behavior:

# Error
sed: 1: invalid command code

# Solution: Use GNU sed
$ brew install gnu-sed
$ export PATH="/opt/homebrew/opt/gnu-sed/libexec/gnubin:$PATH"

Clock Functions

# Error
error: use of undeclared identifier 'CLOCK_MONOTONIC'

# macOS uses different clock APIs
# Fix usually requires code changes or compatibility patches

Undefined Symbols for Architecture

# Error
Undefined symbols for architecture arm64:
  "_some_function", referenced from:

# Solutions:
# 1. Missing library
$ ./configure LDFLAGS="-L/path/to/lib -lmissing"

# 2. Wrong architecture
$ file libsome.dylib  # Check architecture
$ ./configure --build=arm64-apple-darwin

Working with Patches

Applying Patches

# Standard patch
$ patch -p1 < fix.patch

# Dry run (test without applying)
$ patch -p1 --dry-run < fix.patch

# Reverse a patch
$ patch -R -p1 < fix.patch

Common macOS Patches

Many projects have macOS-specific issues. Check:

# Homebrew formula (may contain patches)
$ brew cat software-name

# MacPorts portfile
$ port cat software-name

Creating Patches

# Create patch from changes
$ diff -u original.c modified.c > fix.patch

# From git
$ git diff > changes.patch

Build System Specifics

Make

# Parallel build (faster)
$ make -j$(sysctl -n hw.ncpu)

# Verbose output
$ make V=1
# or
$ make VERBOSE=1

# Clean build
$ make clean
$ make distclean  # Also removes configure output

Autotools Regeneration

If you modify configure.ac or Makefile.am:

# Regenerate configure script
$ autoreconf -fi

# May need:
$ brew install autoconf automake libtool

Installation Locations

Standard Prefixes

PrefixUse
/usr/localTraditional Unix (less common now)
/opt/homebrewHomebrew on Apple Silicon
/opt/localMacPorts
$HOME/.localUser-local installation

Avoiding System Directories

Never install to:

  • /usr (System Integrity Protection)
  • /System (SIP protected)
  • /bin, /sbin (SIP protected)
# This will fail
$ sudo ./configure --prefix=/usr
$ sudo make install
error: Read-only file system

# Use /usr/local or custom prefix instead
$ ./configure --prefix=/usr/local

Environment Setup for Building

Comprehensive Build Environment

# Create a build environment script: build-env.sh

# Homebrew paths
export HOMEBREW_PREFIX="/opt/homebrew"
export PATH="$HOMEBREW_PREFIX/bin:$PATH"

# Compiler flags
export CFLAGS="-I$HOMEBREW_PREFIX/include"
export CXXFLAGS="-I$HOMEBREW_PREFIX/include"
export CPPFLAGS="-I$HOMEBREW_PREFIX/include"
export LDFLAGS="-L$HOMEBREW_PREFIX/lib"

# pkg-config
export PKG_CONFIG_PATH="$HOMEBREW_PREFIX/lib/pkgconfig"

# Prefer Homebrew's versions
export PATH="$HOMEBREW_PREFIX/opt/gnu-sed/libexec/gnubin:$PATH"
export PATH="$HOMEBREW_PREFIX/opt/grep/libexec/gnubin:$PATH"
export PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH"

Using the Environment

$ source build-env.sh
$ ./configure --prefix=/usr/local
$ make -j$(sysctl -n hw.ncpu)

Example: Compiling a Real Package

Let’s walk through compiling tmux from source:

# Install dependencies
$ brew install libevent ncurses

# Download tmux
$ curl -LO https://github.com/tmux/tmux/releases/download/3.4/tmux-3.4.tar.gz
$ tar xzf tmux-3.4.tar.gz
$ cd tmux-3.4

# Configure with Homebrew dependencies
$ ./configure \
    --prefix=/usr/local \
    CPPFLAGS="-I/opt/homebrew/include -I/opt/homebrew/include/ncurses" \
    LDFLAGS="-L/opt/homebrew/lib"

# Build
$ make -j$(sysctl -n hw.ncpu)

# Test (optional)
$ ./tmux -V
tmux 3.4

# Install
$ sudo make install

# Verify
$ /usr/local/bin/tmux -V
tmux 3.4

Static vs Dynamic Linking

Default: Dynamic Linking

$ ./configure
$ make
$ otool -L myprogram
myprogram:
    /usr/lib/libSystem.B.dylib
    /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib
    /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib

Static Linking (where possible)

# Request static linking
$ ./configure --enable-static --disable-shared

# Or link specific libraries statically
$ LDFLAGS="-static-libgcc" ./configure

Note: Full static linking isn’t possible on macOS due to system library requirements.

Troubleshooting Checklist

When a build fails:

  1. Check prerequisites

    $ xcode-select -p
    $ brew doctor
    
  2. Verify dependencies are installed

    $ pkg-config --exists libname && echo "Found"
    
  3. Check config.log

    $ tail -100 config.log  # Shows why configure failed
    
  4. Search for macOS-specific issues

    • Check project’s issue tracker
    • Search Homebrew formula
    • Check MacPorts portfile
  5. Try Homebrew’s formula

    $ brew install --verbose software-name
    # Observe how Homebrew builds it
    
  6. Check architecture

    $ uname -m
    $ file /opt/homebrew/lib/libdependency.dylib
    

Summary

Building from source on macOS:

AspectConsideration
CompilerClang (not GCC)
HeadersMay need explicit paths
LibrariesOften in /opt/homebrew
pkg-configSet PKG_CONFIG_PATH
GNU toolsInstall via Homebrew if needed
Architecturearm64 or x86_64
Installation/usr/local or custom prefix
SIPCan’t write to system directories

The key to successful compilation on macOS is understanding where dependencies are installed and how to tell the build system about them.

Library Paths and Linking

macOS handles dynamic libraries differently from Linux. Understanding these differences is essential for compiling software, debugging linking issues, and distributing applications.

Dynamic Libraries: dylib vs so

PlatformExtensionFormat
macOS.dylibMach-O
Linux.soELF
Windows.dllPE

Library Naming Conventions

# macOS naming
libfoo.dylib                    # Current version symlink
libfoo.1.dylib                  # Major version
libfoo.1.2.3.dylib              # Full version

# Linux naming (for comparison)
libfoo.so                       # Symlink
libfoo.so.1                     # Major version
libfoo.so.1.2.3                 # Full version

# macOS also supports .so for compatibility
# Some ports create .so files, but native libraries use .dylib

Static Libraries

Static libraries use the same format on both platforms:

# Archive format
libfoo.a

# Create static library
$ ar rcs libfoo.a foo.o bar.o

# View contents
$ ar -t libfoo.a
foo.o
bar.o

Library Search Paths

Default Search Order

The dynamic linker (dyld) searches in this order:

  1. DYLD_LIBRARY_PATH (if set and SIP disabled)
  2. LD_LIBRARY_PATH (fallback, if set)
  3. Paths embedded in the binary (rpath, @executable_path, etc.)
  4. /usr/lib
  5. /usr/local/lib

Viewing Search Paths

# Show dyld search paths
$ dyld_info -search_paths /usr/bin/some_binary

# Or check man page for full algorithm
$ man dyld

DYLD_LIBRARY_PATH

Similar to Linux’s LD_LIBRARY_PATH but with restrictions:

# Set library path
$ export DYLD_LIBRARY_PATH=/opt/homebrew/lib:/usr/local/lib

# Run program with modified path
$ DYLD_LIBRARY_PATH=/custom/lib ./myprogram

Important: System Integrity Protection (SIP) clears DYLD_* variables for system binaries and when calling protected executables.

# This won't work for system binaries
$ DYLD_LIBRARY_PATH=/custom sudo somecommand  # Variable cleared

DYLD_FALLBACK_LIBRARY_PATH

Used when library isn’t found in standard locations:

$ export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib:$HOME/lib:/usr/local/lib:/usr/lib

The otool Command

otool is the macOS equivalent of ldd and readelf:

View Linked Libraries

$ otool -L /usr/bin/python3
/usr/bin/python3:
        /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
        /usr/lib/libncurses.5.4.dylib
        /usr/lib/libSystem.B.dylib

# For a Homebrew binary
$ otool -L /opt/homebrew/bin/wget
/opt/homebrew/bin/wget:
        /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib
        /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib
        /opt/homebrew/opt/libidn2/lib/libidn2.0.dylib
        /usr/lib/libz.1.dylib
        /usr/lib/libiconv.2.dylib
        /usr/lib/libSystem.B.dylib

View Load Commands

# All load commands
$ otool -l /opt/homebrew/bin/wget | head -50

# Just the library paths
$ otool -l /opt/homebrew/bin/wget | grep -A2 LC_LOAD_DYLIB
          cmd LC_LOAD_DYLIB
      cmdsize 64
         name /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib

View Symbols

# External symbols
$ nm /opt/homebrew/lib/libssl.dylib | head
0000000000001234 T _SSL_connect
0000000000001456 T _SSL_read
...

# Undefined symbols (needed from other libraries)
$ nm -u /opt/homebrew/bin/wget
                 U _SSL_connect
                 U _SSL_read

Install Names

Every macOS dynamic library has an “install name”—the path recorded inside the library itself:

# View install name
$ otool -D /opt/homebrew/lib/libssl.dylib
/opt/homebrew/lib/libssl.dylib:
/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib

# The install name is embedded in the library
# When you link against it, this path is recorded in your binary

How Install Names Work

  1. Library is built with an install name
  2. When you link against the library, the install name is copied to your binary
  3. At runtime, dyld uses this path to find the library
# Example: Building and linking

# Library specifies its install name during build
$ clang -dynamiclib -install_name /usr/local/lib/libfoo.dylib \
    -o libfoo.dylib foo.c

# Program links against library
$ clang -o myprogram main.c -L. -lfoo

# The install name is recorded
$ otool -L myprogram
myprogram:
        /usr/local/lib/libfoo.dylib  # Install name, not build path!
        /usr/lib/libSystem.B.dylib

install_name_tool

Modify install names in binaries and libraries:

Change Install Name

# Change a library's own install name
$ install_name_tool -id /new/path/libfoo.dylib libfoo.dylib

# Verify
$ otool -D libfoo.dylib
libfoo.dylib:
/new/path/libfoo.dylib

Change Dependency Path

# Change where a binary looks for a library
$ install_name_tool -change \
    /old/path/libfoo.dylib \
    /new/path/libfoo.dylib \
    myprogram

# Verify
$ otool -L myprogram

Add rpath

# Add runtime search path
$ install_name_tool -add_rpath /opt/homebrew/lib myprogram

# Delete rpath
$ install_name_tool -delete_rpath /old/path myprogram

# View rpaths
$ otool -l myprogram | grep -A2 LC_RPATH
          cmd LC_RPATH
      cmdsize 32
         path /opt/homebrew/lib

@executable_path, @loader_path, @rpath

Special path prefixes for relocatable binaries:

@executable_path

Relative to the main executable:

# Install name
@executable_path/../lib/libfoo.dylib

# If executable is /Applications/MyApp.app/Contents/MacOS/MyApp
# Library resolves to /Applications/MyApp.app/Contents/lib/libfoo.dylib

@loader_path

Relative to the binary containing the reference (useful for plugin systems):

# Install name
@loader_path/../Frameworks/libfoo.dylib

# Resolves relative to whatever binary loads this library
# Different from @executable_path for plugins/bundles

@rpath

Resolved using the binary’s rpath list:

# Install name
@rpath/libfoo.dylib

# dyld searches each rpath entry:
# If rpath contains /opt/homebrew/lib, tries /opt/homebrew/lib/libfoo.dylib

Using @rpath

# Build library with @rpath install name
$ clang -dynamiclib -install_name @rpath/libfoo.dylib \
    -o libfoo.dylib foo.c

# Build executable with rpath
$ clang -o myprogram main.c -L. -lfoo \
    -Wl,-rpath,/usr/local/lib \
    -Wl,-rpath,/opt/homebrew/lib

# Multiple rpaths are searched in order
$ otool -l myprogram | grep -A2 LC_RPATH
          cmd LC_RPATH
      cmdsize 32
         path /usr/local/lib
--
          cmd LC_RPATH
      cmdsize 40
         path /opt/homebrew/lib

dyld Debugging

DYLD_PRINT_LIBRARIES

See which libraries are loaded:

$ DYLD_PRINT_LIBRARIES=1 ./myprogram
dyld[12345]: <1A23B456-...> /usr/lib/libSystem.B.dylib
dyld[12345]: <2B34C567-...> /opt/homebrew/lib/libssl.3.dylib
...

DYLD_PRINT_SEARCHING

See the search process:

$ DYLD_PRINT_SEARCHING=1 ./myprogram
dyld[12345]: find path "/opt/homebrew/lib/libfoo.dylib"
dyld[12345]:   found: "/opt/homebrew/lib/libfoo.dylib"

Note: These only work with SIP disabled or for non-system binaries.

dyld_info

Modern tool for analyzing binaries:

# Show all dyld info
$ dyld_info -all /opt/homebrew/bin/wget

# Just dependencies
$ dyld_info -dependents /opt/homebrew/bin/wget

# Exports
$ dyld_info -exports /opt/homebrew/lib/libssl.dylib

Common Linking Problems

Library Not Found

$ ./myprogram
dyld[12345]: Library not loaded: /usr/local/lib/libfoo.dylib
  Referenced from: <UUID> /path/to/myprogram
  Reason: tried: '/usr/local/lib/libfoo.dylib' (no such file)

Solutions:

# 1. Install the library
$ brew install foo

# 2. Create symlink
$ sudo ln -s /opt/homebrew/lib/libfoo.dylib /usr/local/lib/

# 3. Fix the binary's path
$ install_name_tool -change \
    /usr/local/lib/libfoo.dylib \
    /opt/homebrew/lib/libfoo.dylib \
    myprogram

# 4. Add rpath
$ install_name_tool -add_rpath /opt/homebrew/lib myprogram

Wrong Architecture

$ ./myprogram
dyld[12345]: Library not loaded: libfoo.dylib
  Reason: tried: 'libfoo.dylib' (mach-o file, but is an incompatible architecture)

Check architectures:

$ file myprogram
myprogram: Mach-O 64-bit executable arm64

$ file /path/to/libfoo.dylib
libfoo.dylib: Mach-O 64-bit dynamically linked shared library x86_64

# Need to rebuild for matching architecture

Symbol Not Found

$ ./myprogram
dyld[12345]: Symbol not found: _some_function
  Referenced from: <UUID> /path/to/myprogram
  Expected in: <UUID> /path/to/libfoo.dylib

Library version mismatch—library doesn’t have expected symbol:

# Check what symbols library exports
$ nm /path/to/libfoo.dylib | grep some_function

# May need newer library version
$ brew upgrade foo

Creating Dynamic Libraries

Basic Dynamic Library

# Compile to object file
$ clang -c -fPIC foo.c -o foo.o

# Create dynamic library
$ clang -dynamiclib \
    -install_name @rpath/libfoo.dylib \
    -o libfoo.dylib \
    foo.o

# With version info
$ clang -dynamiclib \
    -install_name @rpath/libfoo.1.dylib \
    -current_version 1.2.3 \
    -compatibility_version 1.0.0 \
    -o libfoo.1.2.3.dylib \
    foo.o

# Create symlinks
$ ln -s libfoo.1.2.3.dylib libfoo.1.dylib
$ ln -s libfoo.1.dylib libfoo.dylib

Version Numbers

# View version info
$ otool -L libfoo.dylib
libfoo.dylib:
        @rpath/libfoo.1.dylib (compatibility version 1.0.0, current version 1.2.3)
  • Current version: Actual version of the library
  • Compatibility version: Minimum version needed by binaries linked against it

TBD Files (Text-Based Stubs)

Modern macOS uses text-based stub files instead of actual library binaries in SDKs:

$ cat /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/libSystem.tbd
--- !tapi-tbd
tbd-version:     4
targets:         [ x86_64-macos, arm64-macos ]
install-name:    '/usr/lib/libSystem.B.dylib'
exports:
  - targets:     [ x86_64-macos, arm64-macos ]
    symbols:     [ __exit, _abort, ... ]

TBD files:

  • Contain metadata about library (exports, version)
  • Much smaller than full dylib
  • Used at link time, not runtime
  • Runtime uses actual dylib

Linking at Build Time

Compiler Flags

# Link against library
$ clang -o myprogram main.c -lfoo

# Specify library path
$ clang -o myprogram main.c -L/opt/homebrew/lib -lfoo

# Link against framework
$ clang -o myprogram main.c -framework CoreFoundation

# Specify framework path
$ clang -o myprogram main.c -F/path/to/frameworks -framework MyFramework

# Add rpath at link time
$ clang -o myprogram main.c -lfoo -Wl,-rpath,/opt/homebrew/lib

Static vs Dynamic

# Force static linking of specific library
$ clang -o myprogram main.c /path/to/libfoo.a

# Let linker choose (prefers dynamic)
$ clang -o myprogram main.c -L/path/to/lib -lfoo

# Force all libraries static (not fully supported on macOS)
# macOS always dynamically links system libraries

Summary

ToolPurpose
otool -LView linked libraries
otool -DView install name
otool -lView load commands
nmView symbols
install_name_toolModify paths
dyld_infoModern binary analysis
fileCheck architecture
Path PrefixMeaning
@executable_pathRelative to main executable
@loader_pathRelative to loading binary
@rpathSearch rpath list

Key differences from Linux:

AspectLinuxmacOS
Extension.so.dylib
Path variableLD_LIBRARY_PATHDYLD_LIBRARY_PATH
View dependencieslddotool -L
Modify pathspatchelfinstall_name_tool
Embedded pathRPATH/RUNPATHInstall name

Understanding install names and rpaths is crucial for creating portable macOS applications and debugging library loading issues.

Frameworks vs Unix Libraries

macOS introduces “frameworks”—a packaging concept that bundles libraries, headers, and resources into a single directory structure. Understanding frameworks is essential for macOS development, as they’re used throughout the system and differ significantly from traditional Unix shared libraries.

What Is a Framework?

A framework is a bundle (directory with a specific structure) containing:

  • Dynamic library (the actual code)
  • Header files
  • Resources (images, strings, etc.)
  • Version management
  • Metadata
CoreFoundation.framework/
├── CoreFoundation         → Versions/Current/CoreFoundation
├── Headers                → Versions/Current/Headers/
├── Resources              → Versions/Current/Resources/
└── Versions/
    ├── A/
    │   ├── CoreFoundation      # The actual dylib
    │   ├── Headers/
    │   │   ├── CoreFoundation.h
    │   │   └── ...
    │   └── Resources/
    │       ├── Info.plist
    │       └── ...
    └── Current            → A/

Frameworks vs Traditional Libraries

AspectUnix LibrarymacOS Framework
StructureSingle file + separate headersBundle directory
Headers/usr/include or /usr/local/includeInside framework
VersioningSymlinks (libfoo.so.1)Versions/ directory
ResourcesN/AIncluded
Self-containedNoYes
Discoverabilitypkg-config, manualBuilt-in

Framework Locations

System Frameworks

$ ls /System/Library/Frameworks/
Accelerate.framework/
AppKit.framework/
CoreFoundation.framework/
Foundation.framework/
Security.framework/
...

These are protected by System Integrity Protection.

Local Frameworks

# System-wide third-party frameworks
$ ls /Library/Frameworks/

# User frameworks
$ ls ~/Library/Frameworks/

SDK Frameworks

$ ls $(xcrun --show-sdk-path)/System/Library/Frameworks/

Examining Frameworks

Framework Structure

$ ls -la /System/Library/Frameworks/CoreFoundation.framework/
total 0
lrwxr-xr-x  CoreFoundation -> Versions/Current/CoreFoundation
lrwxr-xr-x  Headers -> Versions/Current/Headers
lrwxr-xr-x  Resources -> Versions/Current/Resources
drwxr-xr-x  Versions/

$ ls /System/Library/Frameworks/CoreFoundation.framework/Versions/
A/
Current    -> A/

Framework Binary

# The binary is inside
$ file /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation: Mach-O universal binary...

# View linked libraries
$ otool -L /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation

Framework Headers

$ ls /System/Library/Frameworks/CoreFoundation.framework/Headers/
CFArray.h
CFBase.h
CFBundle.h
CoreFoundation.h
...

Using Frameworks

Compiling with Frameworks

# Link against framework
$ clang -framework CoreFoundation main.c -o myprogram

# Multiple frameworks
$ clang -framework CoreFoundation -framework Security main.c -o myprogram

# Framework search path
$ clang -F/Library/Frameworks -framework MyFramework main.c

Include Headers

// Import all framework headers
#include <CoreFoundation/CoreFoundation.h>

// Or specific header
#include <CoreFoundation/CFString.h>

// In Objective-C
#import <Foundation/Foundation.h>

// In Swift
import Foundation

Finding Framework Headers

# System frameworks (via SDK)
$ ls $(xcrun --show-sdk-path)/System/Library/Frameworks/CoreFoundation.framework/Headers/

# Include path for compiler
$ clang -F$(xcrun --show-sdk-path)/System/Library/Frameworks ...

Linking Comparison

Traditional Unix Library

# Compile
$ clang -c main.c -I/usr/local/include

# Link
$ clang -o myprogram main.o -L/usr/local/lib -lfoo

# Runtime needs:
# - Library in search path
# - Or DYLD_LIBRARY_PATH set

Framework

# Compile and link
$ clang -framework CoreFoundation main.c -o myprogram

# Runtime:
# - Framework found automatically in standard locations
# - Path embedded in binary

Examining Linked Frameworks

$ otool -L myprogram
myprogram:
        /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
        /usr/lib/libSystem.B.dylib

Creating Your Own Framework

Framework Structure

# Create framework structure
$ mkdir -p MyFramework.framework/Versions/A/{Headers,Resources}
$ cd MyFramework.framework/Versions

# Create symlinks
$ ln -s A Current
$ cd ..
$ ln -s Versions/Current/Headers Headers
$ ln -s Versions/Current/Resources Resources
$ ln -s Versions/Current/MyFramework MyFramework

Build the Library

# Compile source
$ clang -c -fPIC myframework.c -o myframework.o

# Create dynamic library
$ clang -dynamiclib \
    -install_name @rpath/MyFramework.framework/Versions/A/MyFramework \
    -o MyFramework.framework/Versions/A/MyFramework \
    myframework.o

# Copy headers
$ cp myframework.h MyFramework.framework/Versions/A/Headers/

Info.plist

Create MyFramework.framework/Versions/A/Resources/Info.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleIdentifier</key>
    <string>com.example.MyFramework</string>
    <key>CFBundleName</key>
    <string>MyFramework</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundlePackageType</key>
    <string>FMWK</string>
</dict>
</plist>

Using Your Framework

# From parent directory of MyFramework.framework
$ clang -F. -framework MyFramework main.c -o myprogram -Wl,-rpath,@executable_path

Umbrella Frameworks

Some Apple frameworks are “umbrella frameworks” that re-export other frameworks:

// CoreServices includes many sub-frameworks
#include <CoreServices/CoreServices.h>
// This gives you FSEvents, LaunchServices, etc.
$ ls /System/Library/Frameworks/CoreServices.framework/Frameworks/
FSEvents.framework/
LaunchServices.framework/
Metadata.framework/
...

Framework vs dylib: When to Use Which

Use Frameworks When:

  • Distributing macOS/iOS apps
  • Packaging headers with library
  • Including resources (images, plists)
  • Building Apple platform applications
  • Need versioning support

Use dylib When:

  • Porting Unix software
  • Maximum compatibility with Unix build systems
  • Simple library without resources
  • Cross-platform code

XCFrameworks

Modern Apple development uses XCFrameworks for multi-platform support:

MyLibrary.xcframework/
├── ios-arm64/
│   └── MyLibrary.framework/
├── ios-arm64_x86_64-simulator/
│   └── MyLibrary.framework/
└── macos-arm64_x86_64/
    └── MyLibrary.framework/

XCFrameworks bundle frameworks for multiple platforms and architectures.

Creating XCFrameworks

# From multiple framework builds
$ xcodebuild -create-xcframework \
    -framework build/ios/MyFramework.framework \
    -framework build/ios-simulator/MyFramework.framework \
    -framework build/macos/MyFramework.framework \
    -output MyFramework.xcframework

System Framework Details

Core Frameworks

FrameworkPurpose
CoreFoundationC-level foundation (strings, collections)
FoundationObjective-C foundation (NSObject, etc.)
AppKitmacOS GUI
UIKitiOS/iPadOS GUI
SecurityCryptography, keychain
CoreGraphics2D graphics
CoreDataObject persistence
CoreMLMachine learning

Finding Framework Documentation

# Headers are the documentation
$ ls /System/Library/Frameworks/CoreFoundation.framework/Headers/

# Or use Apple's documentation
$ open "https://developer.apple.com/documentation/corefoundation"

Private Frameworks

Apple has private frameworks not meant for public use:

$ ls /System/Library/PrivateFrameworks/

These are:

  • Not documented
  • Subject to change without notice
  • Rejected from App Store
  • Sometimes useful for system utilities

Weak Linking

Link against frameworks that may not exist at runtime:

$ clang -weak_framework NewFramework main.c -o myprogram

In code:

if (@available(macOS 14.0, *)) {
    // Use new framework features
} else {
    // Fallback
}

Framework Search Order

When linking with -framework:

  1. -F specified paths
  2. FRAMEWORK_SEARCH_PATHS in Xcode
  3. System framework directories:
    • $(SDKROOT)/System/Library/Frameworks
    • /System/Library/Frameworks
    • /Library/Frameworks

Common Issues

Framework Not Found

$ clang -framework NonExistent main.c
ld: framework not found NonExistent

Solution:

# Check if framework exists
$ find /System/Library/Frameworks -name "*.framework" | grep -i name

# Add search path
$ clang -F/path/to/frameworks -framework MyFramework main.c

Header Not Found

$ clang main.c
main.c:1:10: fatal error: 'MyFramework/MyFramework.h' file not found

Solution:

# Add framework path (also adds headers)
$ clang -F/path/to/frameworks -framework MyFramework main.c

Old SDK/Missing Symbols

Undefined symbols: "_NewFunction"

Check your SDK version:

$ xcrun --show-sdk-version

# Use newer SDK
$ clang -isysroot $(xcrun --show-sdk-path --sdk macosx14.0) ...

Summary

AspectFrameworkdylib
StructureBundle (directory)Single file
HeadersIncludedSeparate
ResourcesSupportedN/A
VersioningBuilt-inSymlinks
Compile flag-framework Name-lname
Search path-F/path-L/path
Apple APIsRequiredPossible

Frameworks are central to macOS development. While Unix libraries work fine for portable software, frameworks provide a more integrated experience for macOS-native applications.

Debugging with LLDB

LLDB is the debugger for macOS, part of the LLVM project. If you’re coming from Linux and used to GDB, LLDB will feel familiar but different. This chapter covers practical LLDB usage with command equivalents for GDB users.

LLDB vs GDB

FeatureGDBLLDB
PlatformLinux, othersmacOS, iOS, LLVM
ProjectGNULLVM
On macOSAvailable via HomebrewBuilt-in
ScriptingPython, GuilePython
Default on macOSNoYes

LLDB is the only debugger that works seamlessly with macOS code signing, entitlements, and System Integrity Protection.

Getting Started

Launching LLDB

# Debug an executable
$ lldb ./myprogram

# Debug with arguments
$ lldb -- ./myprogram arg1 arg2

# Attach to running process
$ lldb -p 12345

# Attach by name
$ lldb -n Safari

Compiling for Debugging

# Include debug symbols
$ clang -g -O0 program.c -o program

# -g: Generate debug info
# -O0: No optimization (easier debugging)

Essential Commands

Running

TaskLLDBGDB
Run programrun or rrun
Run with argsrun arg1 arg2run arg1 arg2
Continuecontinue or ccontinue
Step overnext or nnext
Step intostep or sstep
Step outfinishfinish
StopCtrl+CCtrl+C
Quitquit or qquit

Breakpoints

# Set breakpoint at function
(lldb) breakpoint set --name main
(lldb) b main                        # Shorthand

# Set breakpoint at file:line
(lldb) breakpoint set --file main.c --line 42
(lldb) b main.c:42                   # Shorthand

# Set breakpoint at address
(lldb) breakpoint set --address 0x100003f00

# Conditional breakpoint
(lldb) breakpoint set --name foo --condition 'x > 10'
(lldb) b foo -c 'x > 10'

# List breakpoints
(lldb) breakpoint list
(lldb) br l

# Delete breakpoint
(lldb) breakpoint delete 1
(lldb) br del 1

# Disable/enable breakpoint
(lldb) breakpoint disable 1
(lldb) breakpoint enable 1

# Delete all breakpoints
(lldb) breakpoint delete

Watchpoints

# Watch variable for write
(lldb) watchpoint set variable myvar

# Watch expression
(lldb) watchpoint set expression -- &myvar

# Watch for read
(lldb) watchpoint set variable -w read myvar

# Watch for read or write
(lldb) watchpoint set variable -w read_write myvar

# List watchpoints
(lldb) watchpoint list

# Delete watchpoint
(lldb) watchpoint delete 1

Examining Data

# Print variable
(lldb) print myvar
(lldb) p myvar

# Print with format
(lldb) print/x myvar          # Hex
(lldb) print/d myvar          # Decimal
(lldb) print/t myvar          # Binary
(lldb) print/c myvar          # Character

# Print expression
(lldb) print 2 + 2
(lldb) print strlen("hello")

# Print pointer contents
(lldb) print *ptr
(lldb) print ptr[0]

# Print array
(lldb) print myarray
(lldb) parray 10 myarray      # Print 10 elements

# Print struct
(lldb) print mystruct
(lldb) print mystruct.field

# Frame variables (local variables)
(lldb) frame variable
(lldb) fr v

# Specific variable
(lldb) frame variable myvar
(lldb) fr v myvar

Memory Examination

# Read memory (x command)
(lldb) memory read 0x100003f00
(lldb) x 0x100003f00

# With format and count
(lldb) memory read --size 4 --count 10 --format x 0x100003f00
(lldb) x -s4 -c10 -fx 0x100003f00

# Read as string
(lldb) memory read --format s 0x100003f00
(lldb) x -fs 0x100003f00

# Read variable's memory
(lldb) memory read &myvar

# Common formats: x (hex), d (decimal), s (string), c (char), i (instruction)

Stack Frames

# Show backtrace
(lldb) thread backtrace
(lldb) bt

# Full backtrace (all threads)
(lldb) thread backtrace all
(lldb) bt all

# Select frame
(lldb) frame select 2
(lldb) f 2

# Frame info
(lldb) frame info

# Up/down frames
(lldb) up
(lldb) down

Threads

# List threads
(lldb) thread list

# Select thread
(lldb) thread select 2

# Thread backtrace
(lldb) thread backtrace

# Continue specific thread
(lldb) thread continue

GDB to LLDB Command Map

TaskGDBLLDB
Runrunrun
Break at functionbreak mainb main
Break at linebreak file:lineb file:line
Continuecontinuecontinue
Step overnextnext
Step intostepstep
Step outfinishfinish
Printprint varprint var
Print hexprint/x varp/x var
Backtracebacktracebt
List breakpointsinfo breakpointsbr list
Delete breakpointdelete 1br del 1
Examine memoryx/10x addrx -c10 -fx addr
Local variablesinfo localsfr v
Disassembledisasdisassemble
Registersinfo registersregister read
Attachattach pidattach -p pid
Set variableset var x=10expr x=10

Advanced Features

Expressions

# Evaluate expression
(lldb) expression 2 + 2
(lldb) expr 2 + 2

# Call function
(lldb) expr (int)printf("Hello\n")

# Modify variable
(lldb) expr myvar = 42

# Cast
(lldb) expr (char *)myptr

# Create variable for session
(lldb) expr int $myval = 42
(lldb) print $myval

Disassembly

# Disassemble current function
(lldb) disassemble
(lldb) di

# Disassemble function by name
(lldb) disassemble --name main
(lldb) di -n main

# Disassemble at address
(lldb) disassemble --start-address 0x100003f00

# Show mixed source and assembly
(lldb) disassemble --mixed
(lldb) di -m

# Disassemble bytes
(lldb) disassemble --bytes
(lldb) di -b

Register Access

# Read all registers
(lldb) register read

# Read specific register
(lldb) register read rax
(lldb) register read x0    # ARM64

# Write register
(lldb) register write rax 0x42

# Read register in different format
(lldb) register read --format binary rax

Process Control

# Process info
(lldb) process status

# Kill process
(lldb) process kill

# Detach
(lldb) process detach

# Signal handling
(lldb) process handle SIGINT --stop false --pass true

Source Code

# Show source
(lldb) source list
(lldb) l

# Show specific lines
(lldb) source list --line 42
(lldb) l -l 42

# Show function
(lldb) source list --name main

macOS-Specific Features

Debugging System Programs

SIP restricts debugging system processes:

# This may fail
$ lldb /usr/bin/some_system_tool
error: process exited with status -1 (attach failed (Not allowed to attach to process. ...))

For system process debugging, disable SIP (not recommended) or debug copies.

Code Signing for Debugging

To debug your own signed apps:

# Check entitlements
$ codesign -d --entitlements - /path/to/app

# May need com.apple.security.get-task-allow entitlement

Debugging Universal Binaries

# Specify architecture
$ lldb --arch x86_64 ./universal_binary
$ lldb --arch arm64 ./universal_binary

Working with dSYM Files

# Debug symbols are in dSYM bundles
$ dsymutil myprogram         # Generate dSYM
$ ls myprogram.dSYM/

# LLDB finds dSYM automatically if nearby
# Or specify manually:
(lldb) target symbols add myprogram.dSYM

Crash Log Analysis

# Symbolicate crash log
$ lldb
(lldb) target create --core /path/to/crashlog

# Or use atos for quick symbolication
$ atos -o myprogram.dSYM/Contents/Resources/DWARF/myprogram -l 0x100000000 0x100003f00

Scripting with Python

Interactive Python

(lldb) script
>>> print(lldb.frame.GetFunctionName())
main
>>> exit()

Python Command

(lldb) script print(lldb.debugger.GetSelectedTarget().GetExecutable())

Custom Commands

Create ~/.lldbinit:

command script import ~/lldb_scripts/mycommands.py

Example script (mycommands.py):

import lldb

def hello_command(debugger, command, result, internal_dict):
    print("Hello from LLDB!")

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f mycommands.hello_command hello')

Configuration

.lldbinit File

Create ~/.lldbinit:

# Custom settings
settings set target.x86-disassembly-flavor intel
settings set stop-line-count-after 5
settings set stop-line-count-before 5

# Aliases
command alias bpl breakpoint list
command alias bpc breakpoint clear

# Auto-load scripts
command script import ~/.lldb/custom.py

# Type formatters
type summary add --summary-string "${var.name} (${var.age} years)" Person

Useful Settings

# Show settings
(lldb) settings list

# Change setting
(lldb) settings set target.run-args arg1 arg2
(lldb) settings set target.env-vars DEBUG=1

# Disassembly flavor
(lldb) settings set target.x86-disassembly-flavor intel

# Auto-confirm
(lldb) settings set auto-confirm true

GUI Mode

LLDB has a curses-based GUI:

(lldb) gui

In GUI mode:

  • Tab: Switch panes
  • Arrow keys: Navigate
  • Enter: Select
  • Esc: Back/close
  • h: Help

Integration with Xcode

LLDB is Xcode’s built-in debugger:

  • Breakpoints set in Xcode use LLDB
  • Debug console is LLDB
  • Can use all LLDB commands in Xcode’s debug console
  • Variable inspection uses LLDB

Common Debugging Scenarios

Debugging Segfault

$ lldb ./crashy_program
(lldb) run
Process stopped
* thread #1, stop reason = EXC_BAD_ACCESS (code=1, address=0x0)

(lldb) bt
* frame #0: crashy_program`bad_function at crashy.c:15
  frame #1: crashy_program`main at crashy.c:42

(lldb) frame select 0
(lldb) frame variable
(lldb) print ptr      # See what's null

Finding Memory Leaks

# Use with MallocStackLogging
$ MallocStackLogging=1 lldb ./program
(lldb) run

# When done, check leaks
$ leaks program_pid

Debugging Deadlocks

(lldb) process interrupt    # Ctrl+C
(lldb) thread list
(lldb) bt all               # See all thread stacks

Summary

CategoryKey Commands
Runningrun, continue, next, step, finish
Breakpointsb function, b file:line, br list, br del
Inspectionprint var, frame variable, bt, memory read
Threadsthread list, thread select, bt all
Memoryx address, memory read
Registersregister read, register write
Controlprocess kill, process detach, quit

LLDB is powerful and integrates deeply with macOS. While the command syntax differs from GDB, the concepts are the same. Use the GDB-to-LLDB mapping table while learning, and soon LLDB’s syntax will feel natural.

Performance Profiling Tools

macOS provides powerful profiling tools for understanding application performance. This chapter covers both command-line tools and Instruments, Apple’s comprehensive profiling application.

Overview of Profiling Tools

ToolPurposeBest For
timeBasic timingQuick measurements
sampleCPU samplingQuick CPU profile
spindumpHang analysisUnresponsive apps
leaksMemory leaksMemory debugging
heapHeap analysisMemory usage
vmmapMemory mapVirtual memory
fs_usageFile systemI/O debugging
dtraceDynamic tracingAdvanced profiling
InstrumentsGUI profilingComprehensive analysis

Basic Timing with time

Built-in time Command

# Bash/zsh built-in
$ time ./myprogram
real    0m1.234s
user    0m0.456s
sys     0m0.078s
  • real: Wall clock time
  • user: CPU time in user mode
  • sys: CPU time in kernel mode

GNU time (more detailed)

# Install
$ brew install gnu-time

# Use with full path or alias
$ /opt/homebrew/bin/gtime -v ./myprogram
        Command being timed: "./myprogram"
        User time (seconds): 0.45
        System time (seconds): 0.07
        Percent of CPU this job got: 42%
        Elapsed (wall clock) time: 1.23
        Maximum resident set size (kbytes): 12800
        ...

Format Output

$ /opt/homebrew/bin/gtime -f "Time: %E\nMemory: %M KB\nCPU: %P" ./myprogram
Time: 0:01.23
Memory: 12800 KB
CPU: 42%

sample - CPU Sampling

sample periodically records call stacks to identify where time is spent:

# Sample for 5 seconds
$ sample myprogram 5

# Sample specific PID
$ sample 12345 5

# Save to file
$ sample myprogram 5 -f output.txt

Sample Output

Sampling process 12345 for 5 seconds with 1 millisecond of run time between samples
Sampling completed, processing symbols...

Analysis of sampling myprogram (pid 12345) every 1 millisecond
Process:         myprogram [12345]
Path:            /path/to/myprogram
Load Address:    0x100000000
...

Call graph:
    2500 Thread_1234   DispatchQueue_1: com.apple.main-thread
      2500 start  (in libdyld.dylib)
        2500 main  (in myprogram)
          1800 expensive_function  (in myprogram)
            1800 calculate_something  (in myprogram)
          700 other_function  (in myprogram)

Total number in stack (recursive counted multiple, when):
        2500       main  (in myprogram)
        1800       expensive_function  (in myprogram)
        1800       calculate_something  (in myprogram)

Understanding Sample Output

The numbers represent how many samples included that function. Higher numbers = more time spent.

spindump - Hang Analysis

spindump captures detailed state when an app is unresponsive:

# Capture hung process
$ sudo spindump myprogram -o spindump.txt

# With duration
$ sudo spindump 12345 5 -o spindump.txt  # 5 second sample

# Sample all processes
$ sudo spindump -notarget -o system_spindump.txt

Triggered Automatically

macOS generates spindumps automatically for hung apps. Find them at:

$ ls ~/Library/Logs/DiagnosticReports/*spin*

Memory Analysis Tools

leaks - Memory Leak Detection

# Check for leaks in running process
$ leaks 12345
Process 12345: 1234 nodes malloced for 567 KB
Process 12345: 2 leaks for 128 bytes

Leak: 0x600000c00100  size=64  zone: DefaultMallocZone_0x108500000
    Call stack: main | allocate_something | malloc

Leak: 0x600000c00140  size=64  zone: DefaultMallocZone_0x108500000
    Call stack: main | another_leak | malloc

Using MallocStackLogging

For detailed leak stacks:

# Run with malloc logging
$ MallocStackLogging=1 ./myprogram &
[1] 12345

# Check leaks with full stacks
$ leaks 12345

# Or use malloc_history
$ malloc_history 12345 0x600000c00100

heap - Heap Analysis

# Summary of heap allocations
$ heap 12345
Process 12345: 3 zones
Zone DefaultMallocZone_0x108500000: Overall size: 2.5MB; 12345 nodes malloced

All zones: 12345 nodes malloced - 2.5MB

Zone DefaultMallocZone_0x108500000: 12345 nodes (2.5MB)
    COUNT     BYTES     AVG   CLASS_NAME
    5000    500000   100.0   non-object
    1234    123400   100.0   CFString
     567     56700   100.0   NSArray

vmmap - Virtual Memory Map

# Memory map overview
$ vmmap 12345

# Summary only
$ vmmap --summary 12345

Process:         myprogram [12345]
Path:            /path/to/myprogram
...

                                VIRTUAL   RESIDENT   DIRTY
REGION TYPE                     SIZE       SIZE     SIZE
===========                   ======     ======   ======
MALLOC                         50.0M     40.0M    35.0M
MALLOC guard page              32K          0K       0K
Stack                          8.0M       200K     200K
__DATA                         2.0M       1.0M     512K
__TEXT                         1.5M       1.5M       0K
...

# Wide output
$ vmmap --wide 12345

File System Tracing

fs_usage - File System Activity

# All filesystem activity
$ sudo fs_usage

# Filter by process
$ sudo fs_usage -w -f filesys myprogram

# Filter by operation type
$ sudo fs_usage -w -f diskio

# Output to file
$ sudo fs_usage myprogram > fs_trace.txt 2>&1

fs_usage Output

14:23:45  open     /path/to/file          0.000234   myprogram
14:23:45  read     F=3            4096    0.000123   myprogram
14:23:45  close    F=3                    0.000012   myprogram

Columns: timestamp, operation, path/details, duration, process

DTrace (Where Available)

DTrace is a powerful dynamic tracing framework. Note: Full DTrace requires disabling SIP on modern macOS.

Basic Usage

# One-liner: trace system calls
$ sudo dtrace -n 'syscall:::entry { @[execname] = count(); }'

# Trace process syscalls
$ sudo dtrace -n 'syscall:::entry /execname == "myprogram"/ { @[probefunc] = count(); }'

# Stop with Ctrl+C to see output
^C
  read                              234
  write                             123
  open                               45

DTrace Scripts

# syscall_count.d
#!/usr/sbin/dtrace -s

syscall:::entry
/execname == "myprogram"/
{
    @syscalls[probefunc] = count();
}

END
{
    printf("\nSystem call counts:\n");
    printa(@syscalls);
}

Run:

$ sudo dtrace -s syscall_count.d

DTrace Limitations on Modern macOS

With SIP enabled:

  • Can only trace user-space
  • Many probes unavailable
  • Cannot trace system processes

For full DTrace access:

  1. Boot to Recovery Mode
  2. csrutil disable (security risk)
  3. Reboot

Not recommended for most users.

Instruments

Instruments is Apple’s comprehensive profiling tool, part of Xcode.

Launching Instruments

# From command line
$ open -a Instruments

# With specific template
$ instruments -t "Time Profiler"

# Profile a command
$ instruments -t "Time Profiler" -D output.trace ./myprogram

Common Instruments Templates

TemplatePurpose
Time ProfilerCPU usage sampling
AllocationsMemory allocation tracking
LeaksMemory leak detection
System TraceComprehensive system events
File ActivityFile system operations
NetworkNetwork connections
Core DataCore Data performance
Animation HitchesUI performance
Metal System TraceGPU profiling

Using Time Profiler

  1. Open Instruments
  2. Choose “Time Profiler”
  3. Select target (app or process)
  4. Click Record
  5. Exercise your code
  6. Click Stop
  7. Analyze call tree

Command-Line Profiling

# Record with instruments
$ xcrun xctrace record --template 'Time Profiler' \
    --launch -- ./myprogram arg1 arg2

# Output creates a .trace file
$ ls *.trace

# Open in Instruments
$ open output.trace

Analyzing Trace Files

# Export to XML
$ xcrun xctrace export --input recording.trace --output results.xml

# List available schemas
$ xcrun xctrace export --input recording.trace --toc

Additional Tools

Activity Monitor Command-Line Equivalent

# top - live process info
$ top

# Sort by CPU
$ top -o cpu

# Sort by memory
$ top -o mem

# Specific process
$ top -pid 12345

System-Wide Memory Pressure

# Memory pressure statistics
$ memory_pressure
The system has 16384 MB of physical memory.
The system has 8192 MB of free memory.
...

# Simulate pressure
$ memory_pressure -l warn

CPU Usage Statistics

# powermetrics (detailed power/performance)
$ sudo powermetrics --samplers cpu_power -n 1
...
CPU Average frequency: 3200 MHz
CPU Percent at or above 3000 MHz: 85%
...

# sysctl for CPU info
$ sysctl -a | grep cpu
hw.ncpu: 8
hw.activecpu: 8
...

Profiling Best Practices

Before Profiling

  1. Build with optimizations (to profile realistic code)
  2. But keep debug symbols (-g flag)
  3. Use Release configuration, not Debug
  4. Have representative test data

During Profiling

  1. Minimize background activity
  2. Close unnecessary applications
  3. Run multiple trials
  4. Use consistent inputs

Interpreting Results

# Get symbol info
$ atos -o ./myprogram -l 0x100000000 0x100003f00
expensive_function (in myprogram) (main.c:42)

Release vs Debug Builds

# Debug build (slow but informative)
$ clang -g -O0 program.c -o program_debug

# Release build (realistic performance)
$ clang -g -O2 program.c -o program_release

# Profile the release build
$ sample program_release 5

Combining Tools

Performance Investigation Workflow

  1. Quick timing: Use time
  2. CPU hotspots: Use sample
  3. Memory issues: Use leaks, heap, vmmap
  4. I/O problems: Use fs_usage
  5. Deep analysis: Use Instruments

Example Investigation

# Step 1: Basic timing
$ time ./myprogram
real    0m5.234s

# Step 2: Where's the time going?
$ sample myprogram 5

# Step 3: Memory usage?
$ vmmap --summary $(pgrep myprogram)

# Step 4: Any leaks?
$ MallocStackLogging=1 ./myprogram &
$ leaks $!

# Step 5: File I/O?
$ sudo fs_usage myprogram

Summary

ToolWhat It ShowsWhen to Use
timeExecution timeQuick benchmarks
sampleCPU call stacksFinding hot functions
spindumpHang stateApp freezes
leaksMemory leaksMemory debugging
heapHeap contentsMemory optimization
vmmapMemory mapMemory layout
fs_usageFile operationsI/O debugging
dtraceDynamic tracingAdvanced analysis
InstrumentsEverythingComprehensive profiling

macOS provides excellent profiling tools at various levels of detail. Start with simple tools (time, sample) and move to more complex ones (Instruments, DTrace) as needed.

Universal Binaries and Apple Silicon

With Apple’s transition from Intel to Apple Silicon, macOS developers need to understand universal binaries—single executable files containing code for multiple architectures. This chapter covers building, inspecting, and managing universal binaries.

Architecture Background

The Transition

PeriodMac ArchitectureIdentifier
2006-2020Intel 64-bitx86_64
2020-presentApple Siliconarm64
TransitionBothUniversal

Architecture Identifiers

# Check current machine architecture
$ uname -m
arm64      # Apple Silicon
# or
x86_64     # Intel

# Check process architecture
$ arch
arm64

# Full system info
$ uname -a
Darwin hostname 23.4.0 Darwin Kernel Version 23.4.0: ... RELEASE_ARM64_T6000 arm64

What Is a Universal Binary?

A universal binary (or “fat binary”) contains:

  • Multiple architecture-specific code sections
  • Metadata describing each architecture
  • Single file, multiple executables inside
Universal Binary
┌─────────────────────────────────────┐
│ Fat Header                          │
│   - Magic number                    │
│   - Number of architectures: 2      │
│   - Architecture entries            │
├─────────────────────────────────────┤
│ arm64 Mach-O executable             │
│   - Code compiled for Apple Silicon │
├─────────────────────────────────────┤
│ x86_64 Mach-O executable            │
│   - Code compiled for Intel         │
└─────────────────────────────────────┘

Inspecting Binaries

Using file

# Single architecture (Intel)
$ file /usr/bin/some_intel_app
/usr/bin/some_intel_app: Mach-O 64-bit executable x86_64

# Single architecture (Apple Silicon)
$ file /usr/bin/some_arm_app
/usr/bin/some_arm_app: Mach-O 64-bit executable arm64

# Universal binary
$ file /bin/ls
/bin/ls: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]

# Homebrew binary (usually single arch)
$ file /opt/homebrew/bin/wget
/opt/homebrew/bin/wget: Mach-O 64-bit executable arm64

Using lipo

lipo is the main tool for working with universal binaries:

# List architectures
$ lipo -info /bin/ls
Architectures in the fat file: /bin/ls are: x86_64 arm64e

# Detailed info
$ lipo -detailed_info /bin/ls
Fat header in: /bin/ls
fat_magic 0xcafebabe
nfat_arch 2
architecture x86_64
    cputype CPU_TYPE_X86_64
    cpusubtype CPU_SUBTYPE_X86_64_ALL
    capabilities 0x0
    offset 16384
    size 72672
    align 2^14 (16384)
architecture arm64e
    cputype CPU_TYPE_ARM64
    cpusubtype CPU_SUBTYPE_ARM64E
    capabilities PTR_AUTH_VERSION USERSPACE 0
    offset 98304
    size 72832
    align 2^14 (16384)

arm64 vs arm64e

# arm64: Standard Apple Silicon
# arm64e: Enhanced with Pointer Authentication (PAC)

# System binaries often use arm64e
$ file /bin/ls
/bin/ls: ... arm64e

# Third-party typically use arm64
$ file /opt/homebrew/bin/node
/opt/homebrew/bin/node: Mach-O 64-bit executable arm64

Building Universal Binaries

Compiler Flags

# Build for single architecture
$ clang -arch arm64 program.c -o program_arm64
$ clang -arch x86_64 program.c -o program_x86_64

# Build universal binary directly
$ clang -arch arm64 -arch x86_64 program.c -o program_universal

# Verify
$ file program_universal
program_universal: Mach-O universal binary with 2 architectures: [x86_64:...] [arm64:...]

Using lipo to Combine

# Build separate binaries
$ clang -arch arm64 program.c -o program_arm64
$ clang -arch x86_64 program.c -o program_x86_64

# Combine into universal
$ lipo -create program_arm64 program_x86_64 -output program_universal

# Verify
$ lipo -info program_universal
Architectures in the fat file: program_universal are: x86_64 arm64

CMake

# CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(MyProject)

# Build universal binary
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")

add_executable(myprogram main.c)

Build:

$ mkdir build && cd build
$ cmake ..
$ make
$ file myprogram
myprogram: Mach-O universal binary with 2 architectures: [x86_64:...] [arm64:...]

Autotools/Configure

# Set compiler flags
$ CFLAGS="-arch arm64 -arch x86_64" \
  LDFLAGS="-arch arm64 -arch x86_64" \
  ./configure --prefix=/usr/local

$ make

Xcode

In Xcode Build Settings:

  • Architectures: Standard Architectures (arm64, x86_64)
  • Or set ARCHS = arm64 x86_64

Extracting Architectures

Extract Single Architecture

# Extract arm64 version
$ lipo -extract arm64 program_universal -output program_arm64_only

# Extract x86_64 version
$ lipo -thin x86_64 program_universal -output program_x86_64_only

# Verify
$ file program_arm64_only
program_arm64_only: Mach-O 64-bit executable arm64

Remove Architecture

# Remove x86_64, keep arm64
$ lipo -remove x86_64 program_universal -output program_arm64_only

Rosetta 2

Rosetta 2 translates x86_64 code to run on Apple Silicon:

Check if Running Under Rosetta

# From inside a process
$ sysctl -n sysctl.proc_translated
1    # Running under Rosetta
0    # Native arm64

# Shell check
if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then
    echo "Running under Rosetta 2"
else
    echo "Running natively"
fi

Force x86_64 Execution

# Run Intel binary via Rosetta
$ arch -x86_64 ./intel_program

# Start shell in x86_64 mode
$ arch -x86_64 /bin/zsh

# Then all commands run as x86_64
$ uname -m
x86_64

Installing Rosetta

# Rosetta installs automatically when needed
# Or install manually:
$ softwareupdate --install-rosetta

# Non-interactive
$ softwareupdate --install-rosetta --agree-to-license

Rosetta Limitations

  • Performance overhead (typically 70-90% of native)
  • Some instructions not supported
  • Kernel extensions don’t translate
  • Virtual Machine hypervisor features differ

Homebrew and Architecture

Separate Installations

Homebrew uses different prefixes by architecture:

ArchitecturePrefix
Apple Silicon/opt/homebrew
Intel/usr/local
# Native Apple Silicon Homebrew
$ /opt/homebrew/bin/brew --prefix
/opt/homebrew

# Intel Homebrew (under Rosetta)
$ arch -x86_64 /usr/local/bin/brew --prefix
/usr/local

Installing Both

# Native ARM Homebrew (on Apple Silicon)
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Intel Homebrew (optional, for x86_64 packages)
$ arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Checking Package Architecture

# Check what arch a Homebrew package is
$ file /opt/homebrew/bin/python3
/opt/homebrew/bin/python3: Mach-O 64-bit executable arm64

$ file /usr/local/bin/python3
/usr/local/bin/python3: Mach-O 64-bit executable x86_64

Development Considerations

Conditional Compilation

#if defined(__arm64__) || defined(__aarch64__)
    // Apple Silicon specific code
    printf("Running on Apple Silicon\n");
#elif defined(__x86_64__)
    // Intel specific code
    printf("Running on Intel\n");
#endif

Runtime Detection

#include <sys/sysctl.h>

int is_apple_silicon() {
    int ret = 0;
    size_t size = sizeof(ret);
    // Check if running natively on ARM
    if (sysctlbyname("hw.optional.arm64", &ret, &size, NULL, 0) == 0) {
        return ret;
    }
    return 0;
}

int is_translated() {
    int ret = 0;
    size_t size = sizeof(ret);
    // Check if running under Rosetta
    if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == 0) {
        return ret;
    }
    return 0;
}

Libraries and Linking

When building universal binaries, all linked libraries must support both architectures:

# Check library architecture
$ file /opt/homebrew/lib/libssl.dylib
/opt/homebrew/lib/libssl.dylib: Mach-O 64-bit dynamically linked shared library arm64

# Error when mixing architectures
$ clang -arch x86_64 program.c -L/opt/homebrew/lib -lssl
ld: warning: ignoring file /opt/homebrew/lib/libssl.dylib: building for macOS-x86_64 but attempting to link with file built for macOS-arm64

Solution: Use universal libraries or build architecture-specific binaries.

Debugging Universal Binaries

LLDB Architecture Selection

# Debug specific architecture
$ lldb --arch x86_64 ./universal_program
$ lldb --arch arm64 ./universal_program

Crash Reports

Check architecture in crash reports:

Process:         myprogram [12345]
...
Code Type:       ARM-64 (Native)
# or
Code Type:       X86-64 (Translated)

Distribution

App Store

  • Universal binaries recommended
  • Apple Silicon required for new apps
  • Rosetta support continues for existing Intel apps

Direct Distribution

# Check your binary before distribution
$ file MyApp.app/Contents/MacOS/MyApp
MyApp.app/Contents/MacOS/MyApp: Mach-O universal binary with 2 architectures...

# Notarize (works with universal binaries)
$ xcrun notarytool submit MyApp.zip --apple-id ... --team-id ...

Reducing Binary Size

Universal binaries are larger. To distribute architecture-specific:

# Create arm64-only version for Apple Silicon
$ lipo -thin arm64 MyApp -output MyApp_arm64

# Compare sizes
$ ls -lh MyApp MyApp_arm64
-rwxr-xr-x  1 user  staff   2.0M  MyApp
-rwxr-xr-x  1 user  staff   1.0M  MyApp_arm64

Common Issues

“Bad CPU Type” Error

$ ./program
Bad CPU type in executable

Causes:

  • Running arm64 binary on Intel Mac
  • Running x86_64 binary on Apple Silicon without Rosetta

Solutions:

# Install Rosetta (Apple Silicon)
$ softwareupdate --install-rosetta

# Or rebuild for correct architecture
$ clang -arch arm64 program.c -o program

Architecture Mismatch in Libraries

ld: building for macOS-arm64 but attempting to link with file built for macOS-x86_64

Solution: Ensure all libraries match target architecture, or build truly universal libraries.

Performance Profiling

When profiling universal binaries, ensure you’re testing the native architecture:

# On Apple Silicon
$ arch
arm64

# Force native execution (already default)
$ arch -arm64 ./program

# Profile
$ instruments -t "Time Profiler" ./program

Summary

TaskCommand
Check architecturefile binary or lipo -info binary
Build universalclang -arch arm64 -arch x86_64 ...
Combine binarieslipo -create arm64_bin x86_64_bin -output universal
Extract architecturelipo -thin arm64 universal -output arm64_only
Run as Intelarch -x86_64 ./program
Check if translatedsysctl -n sysctl.proc_translated

Understanding universal binaries is essential for:

  • Supporting both Intel and Apple Silicon Macs
  • Proper library linking
  • Performance optimization
  • Application distribution

As the Mac ecosystem transitions fully to Apple Silicon, the need for x86_64 support will diminish, but understanding these concepts remains important for maintaining existing software and working with cross-platform codebases.

Administering macOS

System administration on macOS blends Unix tradition with Apple’s unique approaches. If you’re coming from Linux, many concepts will feel familiar, but the tools and methods differ significantly. This part covers what you need to know to effectively manage macOS systems from the command line.

macOS Administration Philosophy

macOS administration differs from traditional Unix in several key ways:

AspectTraditional UnixmacOS
User management/etc/passwd, useraddDirectory Services, dscl, sysadminctl
System permissionsStandard Unix permissionsUnix permissions + ACLs + SIP
Firewalliptables, nftablespf (from OpenBSD)
Loggingsyslog, journaldUnified Logging (log command)
ConfigurationText config filesProperty lists (plists), defaults
Startupsystemd, rc.dlaunchd (covered in Part V)
RecoverySingle-user modeRecovery Mode partition

The Command Line Administrator

macOS is fully administrable from the terminal. While System Settings provides a GUI, every configuration can be modified from the command line:

# User management
$ sudo sysadminctl -addUser newuser -fullName "New User" -password -

# Firewall control
$ sudo pfctl -e    # Enable firewall

# View system logs
$ log show --predicate 'eventMessage contains "error"' --last 1h

# Read/write preferences
$ defaults read com.apple.finder
$ defaults write com.apple.dock autohide -bool true

# Check SIP status
$ csrutil status
System Integrity Protection status: enabled.

Privilege Escalation

macOS uses the standard Unix sudo mechanism, but with Apple’s authentication framework:

# Standard sudo
$ sudo cat /etc/sudoers
Password: ********

# Check sudo privileges
$ sudo -l
User admin may run the following commands on hostname:
    (ALL) ALL

# Run as different user
$ sudo -u www whoami
www

# Edit with sudo
$ sudo visudo

The sudo timeout is typically 5 minutes. You can adjust it in /etc/sudoers:

# Extend timeout to 15 minutes
Defaults timestamp_timeout=15

# Require password every time
Defaults timestamp_timeout=0

System Information Quick Reference

Essential commands for understanding your system:

# macOS version
$ sw_vers
ProductName:		macOS
ProductVersion:		14.2
BuildVersion:		23C64

# Hardware overview
$ system_profiler SPHardwareDataType
Hardware:

    Hardware Overview:

      Model Name: MacBook Pro
      Model Identifier: Mac14,5
      Chip: Apple M2 Pro
      Total Number of Cores: 12 (8 performance and 4 efficiency)
      Memory: 32 GB
      System Firmware Version: 10151.61.4
      OS Loader Version: 10151.61.4
      Serial Number (system): XXXXXXXXXXXX
      Hardware UUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

# CPU information
$ sysctl -n machdep.cpu.brand_string
Apple M2 Pro

# Memory
$ sysctl hw.memsize
hw.memsize: 34359738368

# Disk usage
$ df -h
Filesystem       Size   Used  Avail Capacity iused ifree %iused  Mounted on
/dev/disk3s1s1  926Gi   14Gi  756Gi     2%    404k  7.9G    0%   /
/dev/disk3s6    926Gi  3.0Gi  756Gi     1%       3  7.9G    0%   /System/Volumes/VM
/dev/disk3s2    926Gi  9.5Gi  756Gi     2%    1.4k  7.9G    0%   /System/Volumes/Preboot
/dev/disk3s4    926Gi  143Gi  756Gi    16%    1.9M  7.9G    0%   /System/Volumes/Data

# Uptime
$ uptime
10:30  up 5 days, 12:45, 3 users, load averages: 1.75 2.20 2.10

What You’ll Learn in This Part

User and Group Management covers how macOS handles users and groups through Directory Services, including dscl, sysadminctl, and the relationship between local accounts and directory services.

The Permissions Model: Unix Meets ACLs explains how macOS combines traditional Unix permissions with Access Control Lists (ACLs) for fine-grained access control.

System Integrity Protection (SIP) describes Apple’s security feature that protects system files and processes, and when you might need to (carefully) disable it.

Configuring the pf Firewall shows how to use the powerful pf firewall inherited from OpenBSD, including rules, tables, and persistent configuration.

Unified Logging System teaches you to use the log command to query macOS’s modern logging system, which replaced traditional syslog.

System Configuration via defaults demonstrates how to read and write macOS preferences from the command line, enabling powerful system customization.

Startup and Boot Process walks through the macOS boot sequence from firmware through launchd, helping you understand and troubleshoot startup issues.

Recovery Mode and Troubleshooting covers accessing Recovery Mode, using its Terminal, reinstalling macOS, and resetting passwords when needed.

Administrator’s Toolkit

A quick reference of essential administration commands:

# System Management
sudo shutdown -h now          # Shutdown immediately
sudo shutdown -r now          # Restart immediately
sudo shutdown -h +60          # Shutdown in 60 minutes
sudo reboot                   # Restart
sudo systemsetup -setcomputersleep 60    # Sleep after 60 minutes

# Service Management
sudo launchctl list           # List all services
sudo launchctl load /path     # Load service
sudo launchctl unload /path   # Unload service

# User Management
dscl . list /Users            # List users
dscl . read /Users/username   # Read user details
sudo sysadminctl -addUser     # Add user

# Disk Management
diskutil list                 # List disks
diskutil info disk0           # Disk information
sudo diskutil repairDisk disk0  # Repair disk

# Security
csrutil status                # SIP status
spctl --status                # Gatekeeper status
sudo pfctl -s info            # Firewall info

Remote Administration

macOS supports remote administration via SSH and Apple Remote Desktop (ARD):

# Enable SSH (Remote Login)
$ sudo systemsetup -setremotelogin on
$ sudo systemsetup -getremotelogin
Remote Login: On

# Enable ARD for specific users
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -activate -configure -access -on -users admin -privs -all -restart -agent

# Check ARD status
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -agent -print

# Disable ARD
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -deactivate -stop

The following chapters dive deep into each aspect of macOS system administration, giving you the knowledge to manage macOS systems as effectively from the terminal as any Unix administrator manages their Linux servers.

User and Group Management

macOS doesn’t use /etc/passwd and useradd like Linux. Instead, it relies on Directory Services, a flexible system that can manage local accounts and integrate with directory servers like Active Directory or LDAP. Understanding this difference is essential for macOS system administration.

Directory Services Architecture

On macOS, all user and group information flows through Directory Services (previously called Open Directory):

Application
    │
    ▼
Directory Services API
    │
    ├── Local Directory (/var/db/dslocal/)
    │       └── Local users, groups, computers
    │
    ├── Active Directory
    │       └── Domain users, groups
    │
    └── LDAP
            └── Network directory users

The key commands for interacting with Directory Services are:

CommandPurpose
dsclDirectory Service command line utility
sysadminctlModern user management tool (10.10+)
dseditgroupGroup membership management
dscacheutilQuery and flush directory cache
idDisplay user and group IDs

Understanding UIDs and GIDs

macOS uses Unix UIDs (User IDs) and GIDs (Group IDs), but with some Apple-specific conventions:

# View your UID and GIDs
$ id
uid=501(david) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),
79(_appserverusr),80(admin),81(_appserveradm),98(_lpadmin),701(com.apple.sharepoint.group.1),
33(_appstore),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),
398(com.apple.access_screensharing),399(com.apple.access_ssh)

# View another user's info
$ id -P admin
admin:********:501:20::0:0:Admin User:/Users/admin:/bin/zsh

Standard macOS UID ranges:

UID RangePurpose
0root
1-199System accounts (daemon, nobody, etc.)
200-400System services
401-500Reserved
501+Regular users

First user created during setup gets UID 501:

$ dscl . -read /Users/david UniqueID
UniqueID: 501

Listing Users and Groups

Using dscl

# List all local users
$ dscl . -list /Users
_amavisd
_appleevents
_applepay
...
daemon
david
Guest
nobody
root

# List users with UIDs
$ dscl . -list /Users UniqueID
_amavisd                211
_appleevents            55
_applepay               260
...
david                   501
root                    0

# List only "real" users (UID >= 500)
$ dscl . -list /Users UniqueID | awk '$2 >= 500 {print $1}'
david
admin

# List all local groups
$ dscl . -list /Groups
admin
everyone
staff
wheel
...

# List groups with GIDs
$ dscl . -list /Groups PrimaryGroupID
admin                   80
everyone                12
staff                   20
wheel                   0

Using dscacheutil

# Query user information
$ dscacheutil -q user -a name david
name: david
password: ********
uid: 501
gid: 20
dir: /Users/david
shell: /bin/zsh
gecos: David

# Query group
$ dscacheutil -q group -a name admin
name: admin
password: *
gid: 80
users: root david

# Flush directory cache
$ sudo dscacheutil -flushcache

Reading User Attributes

Each user has many attributes in Directory Services:

# Read all attributes for a user
$ dscl . -read /Users/david
AppleMetaNodeLocation: /Local/Default
GeneratedUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
NFSHomeDirectory: /Users/david
Password: ********
PrimaryGroupID: 20
RealName: David
RecordName: david
RecordType: dsRecTypeStandard:Users
UniqueID: 501
UserShell: /bin/zsh
...

# Read specific attribute
$ dscl . -read /Users/david RealName
RealName: David

$ dscl . -read /Users/david UserShell
UserShell: /bin/zsh

$ dscl . -read /Users/david NFSHomeDirectory
NFSHomeDirectory: /Users/david

# Check if user is admin
$ dscl . -read /Groups/admin GroupMembership
GroupMembership: root david

# Raw value only
$ dscl . -read /Users/david UniqueID | awk '{print $2}'
501

Creating Users

sysadminctl is the modern tool for user management:

# Create a standard user (interactive password)
$ sudo sysadminctl -addUser newuser -fullName "New User" -password -
Enter password for new user newuser:
Confirm password for new user newuser:
Creating user record...
User record created successfully.

# Create admin user
$ sudo sysadminctl -addUser newadmin -fullName "New Admin" -password - -admin

# Create user with specific UID
$ sudo sysadminctl -addUser newuser -fullName "New User" -UID 601 -password -

# Create user with specified shell
$ sudo sysadminctl -addUser newuser -fullName "New User" -shell /bin/bash -password -

# Create hidden user (doesn't appear on login screen)
$ sudo sysadminctl -addUser hiddenuser -fullName "Hidden User" -password - -admin
$ sudo dscl . create /Users/hiddenuser IsHidden 1

Using dscl (Manual Method)

For more control, create users with dscl:

# Find next available UID
$ dscl . -list /Users UniqueID | awk '{print $2}' | sort -n | tail -1
501
# So next UID would be 502

# Create user record
$ sudo dscl . -create /Users/newuser

# Set required attributes
$ sudo dscl . -create /Users/newuser UserShell /bin/zsh
$ sudo dscl . -create /Users/newuser RealName "New User"
$ sudo dscl . -create /Users/newuser UniqueID 502
$ sudo dscl . -create /Users/newuser PrimaryGroupID 20
$ sudo dscl . -create /Users/newuser NFSHomeDirectory /Users/newuser

# Set password
$ sudo dscl . -passwd /Users/newuser
New Password:

# Create home directory
$ sudo createhomedir -c -u newuser

# Or manually create home directory from template
$ sudo cp -R /System/Library/User\ Template/English.lproj /Users/newuser
$ sudo chown -R newuser:staff /Users/newuser

Modifying Users

# Change user's full name
$ sudo dscl . -change /Users/david RealName "David" "David Smith"

# Change shell
$ sudo dscl . -change /Users/david UserShell /bin/zsh /bin/bash

# Or create/replace attribute
$ sudo dscl . -create /Users/david UserShell /bin/bash

# Change password
$ sudo dscl . -passwd /Users/david
New Password:

# User changes own password
$ passwd
Changing password for david.
Old Password:
New Password:
Retype New Password:

# Disable user (prevent login)
$ sudo pwpolicy -u newuser -setpolicy "isDisabled=1"

# Re-enable user
$ sudo pwpolicy -u newuser -setpolicy "isDisabled=0"

Deleting Users

# Delete user with sysadminctl
$ sudo sysadminctl -deleteUser olduser
Deleting user record...
User "olduser" deleted.

# Delete user AND home directory
$ sudo sysadminctl -deleteUser olduser -secure

# Delete user with dscl
$ sudo dscl . -delete /Users/olduser

# Manually remove home directory
$ sudo rm -rf /Users/olduser

Group Management

Viewing Groups

# List all groups
$ dscl . -list /Groups PrimaryGroupID
admin                   80
daemon                  1
everyone                12
kmem                    2
localaccounts           61
mail                    6
network                 69
nobody                  -2
nogroup                 -1
operator                5
staff                   20
sys                     3
tty                     4
utmp                    45
wheel                   0
...

# Read group details
$ dscl . -read /Groups/admin
AppleMetaNodeLocation: /Local/Default
GeneratedUID: ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000050
GroupMembership: root david
Password: *
PrimaryGroupID: 80
RealName: Administrators
RecordName: admin BUILTIN\Administrators
RecordType: dsRecTypeStandard:Groups

# List group members
$ dscl . -read /Groups/admin GroupMembership
GroupMembership: root david

# Check if user is in group
$ dseditgroup -o checkmember -m david admin
yes david is a member of admin

Creating Groups

# Create new group
$ sudo dscl . -create /Groups/developers

# Set group ID
$ sudo dscl . -create /Groups/developers PrimaryGroupID 1001

# Set group name
$ sudo dscl . -create /Groups/developers RealName "Developers"

# Add group password (rarely needed)
$ sudo dscl . -create /Groups/developers Password "*"

Managing Group Membership

# Add user to group (using dseditgroup)
$ sudo dseditgroup -o edit -a david -t user admin
$ sudo dseditgroup -o edit -a david -t user developers

# Remove user from group
$ sudo dseditgroup -o edit -d david -t user developers

# Add user to group (using dscl)
$ sudo dscl . -append /Groups/developers GroupMembership david

# Remove user from group (using dscl)
$ sudo dscl . -delete /Groups/developers GroupMembership david

# List all groups a user belongs to
$ id -Gn david
staff everyone localaccounts _appserverusr admin _appserveradm _lpadmin com.apple.sharepoint.group.1 _appstore _lpoperator _developer _analyticsusers com.apple.access_ftp com.apple.access_screensharing com.apple.access_ssh

Deleting Groups

# Delete group
$ sudo dscl . -delete /Groups/developers

Special macOS Groups

macOS has several important groups:

GroupGIDPurpose
wheel0Traditional Unix superuser group
admin80macOS administrators (can use sudo)
staff20Default group for regular users
everyone12All users including guests
localaccounts61All local (non-network) accounts
_developer204Can debug other processes

Adding a user to admin makes them a macOS administrator:

# Make user an admin
$ sudo dseditgroup -o edit -a username -t user admin

# Remove admin rights
$ sudo dseditgroup -o edit -d username -t user admin

# Verify admin status
$ dseditgroup -o checkmember -m username admin

Working with Network Directories

Checking Directory Configuration

# List configured directory nodes
$ dscl -list /
Active Directory
BSD Configuration and target
Cache
Local
Search
Contact Search

# Read from specific node
$ dscl "/Active Directory/DOMAIN" -list /Users

Active Directory Integration

# Check AD binding status
$ dsconfigad -show
Active Directory Forest          = corp.example.com
Active Directory Domain          = corp.example.com
Computer Account                 = MY-MAC$
...

# Bind to Active Directory (run from GUI or carefully from CLI)
$ sudo dsconfigad -add corp.example.com -username admin -password - -computer MY-MAC

# Remove AD binding
$ sudo dsconfigad -remove -username admin -password -

# Force AD cache refresh
$ sudo dscacheutil -flushcache

Directory Service Paths

# Local directory data location
/var/db/dslocal/

# Per-node data
/var/db/dslocal/nodes/Default/
    ├── aliases/
    ├── computers/
    ├── config/
    ├── groups/
    └── users/

# User plist files
$ sudo ls /var/db/dslocal/nodes/Default/users/
_amavisd.plist
_appleevents.plist
daemon.plist
david.plist
nobody.plist
root.plist

Common Administration Tasks

Reset a User’s Password

# As admin, reset another user's password
$ sudo dscl . -passwd /Users/targetuser newpassword

# Or interactively
$ sudo dscl . -passwd /Users/targetuser
New Password:

# Using sysadminctl
$ sudo sysadminctl -resetPasswordFor targetuser -newPassword -

Find All Admin Users

# List all administrators
$ dscl . -read /Groups/admin GroupMembership
GroupMembership: root david admin2

# More detailed
$ dscl . -read /Groups/admin GroupMembership | tr ' ' '\n' | tail -n +2
root
david
admin2

Create Service Account

Service accounts typically have no home directory and can’t log in:

# Create service account
$ sudo dscl . -create /Users/_myservice
$ sudo dscl . -create /Users/_myservice UserShell /usr/bin/false
$ sudo dscl . -create /Users/_myservice RealName "My Service"
$ sudo dscl . -create /Users/_myservice UniqueID 401
$ sudo dscl . -create /Users/_myservice PrimaryGroupID 401
$ sudo dscl . -create /Users/_myservice NFSHomeDirectory /var/empty

# Create matching group
$ sudo dscl . -create /Groups/_myservice
$ sudo dscl . -create /Groups/_myservice PrimaryGroupID 401

# Set password to * (can't login)
$ sudo dscl . -create /Users/_myservice Password "*"

List Users Who Can SSH

# SSH access is controlled by the com.apple.access_ssh group
$ dscl . -read /Groups/com.apple.access_ssh GroupMembership 2>/dev/null || echo "Group doesn't exist - all users can SSH"

# Add user to SSH access group
$ sudo dseditgroup -o edit -a username -t user com.apple.access_ssh

# Remove SSH access
$ sudo dseditgroup -o edit -d username -t user com.apple.access_ssh

Comparison with Linux

TaskLinuxmacOS
List userscat /etc/passwddscl . -list /Users
Add useruseraddsysadminctl -addUser
Delete useruserdelsysadminctl -deleteUser
Modify userusermoddscl . -change
Add to groupusermod -aG group userdseditgroup -o edit -a user -t user group
List groupscat /etc/groupdscl . -list /Groups
Change passwordpasswd userdscl . -passwd /Users/user
User infoid userid user (same)

Summary

macOS user management through Directory Services is more complex than Linux’s /etc/passwd approach, but offers advantages:

  • Unified interface for local and network accounts
  • Rich metadata on user records
  • Integration with enterprise directories
  • Consistent API across different backends

Key commands to remember:

CommandPurpose
dscl . -list /UsersList all users
dscl . -read /Users/nameRead user details
sysadminctl -addUserCreate new user
sysadminctl -deleteUserDelete user
dseditgroup -o edit -a user -t user groupAdd to group
id usernameShow UID/GIDs
dscacheutil -flushcacheClear directory cache

Master these tools, and you’ll manage macOS users as effectively as any Linux sysadmin manages their systems.

The Permissions Model: Unix Meets ACLs

macOS implements a layered permission system that combines traditional Unix permissions with Access Control Lists (ACLs). Understanding both layers is essential for proper file security management, especially when troubleshooting access issues.

The Three Layers of Access Control

macOS evaluates file access through three layers:

1. System Integrity Protection (SIP)
        │
        ▼ (if allowed)
2. Access Control Lists (ACLs)
        │
        ▼ (if no ACL match)
3. Traditional Unix Permissions

This chapter focuses on layers 2 and 3. SIP is covered in its own chapter.

Traditional Unix Permissions

The Basics

Unix permissions use a three-tier model:

$ ls -l document.txt
-rw-r--r--  1 david  staff  1024 Jan 15 10:00 document.txt

Breaking down -rw-r--r--:

PositionMeaning
1File type (- file, d directory, l symlink)
2-4Owner permissions (user)
5-7Group permissions
8-10Other permissions (everyone else)

Permission bits:

SymbolOctalMeaning for FilesMeaning for Directories
r4Read contentsList contents
w2Modify contentsCreate/delete files
x1ExecuteEnter (cd into)

Reading Permissions

# Long listing
$ ls -l /Users/david/
total 0
drwx------+  5 david  staff   160 Jan 15 10:00 Desktop
drwx------+  8 david  staff   256 Jan 15 10:00 Documents
drwx------+  3 david  staff    96 Jan 15 10:00 Downloads
drwx------@ 85 david  staff  2720 Jan 15 10:00 Library
drwx------   6 david  staff   192 Jan 15 10:00 Movies
drwx------+  4 david  staff   128 Jan 15 10:00 Music
drwx------+  5 david  staff   160 Jan 15 10:00 Pictures
drwxr-xr-x+  4 david  staff   128 Jan 15 10:00 Public

# Note the + and @ symbols:
# + indicates ACLs present
# @ indicates extended attributes present

Numeric (Octal) Permissions

Each permission set converts to a number 0-7:

# Calculate: r(4) + w(2) + x(1)
rwx = 4+2+1 = 7
rw- = 4+2+0 = 6
r-x = 4+0+1 = 5
r-- = 4+0+0 = 4
--- = 0+0+0 = 0

# Common permission sets
-rw-r--r--  = 644  (owner read/write, others read)
-rwxr-xr-x  = 755  (executable, world readable)
-rw-------  = 600  (owner only)
drwx------  = 700  (private directory)
drwxr-xr-x  = 755  (public directory)

Changing Permissions with chmod

# Symbolic mode
$ chmod u+x script.sh           # Add execute for owner
$ chmod g-w file.txt            # Remove write for group
$ chmod o=r file.txt            # Set others to read only
$ chmod a+r file.txt            # Add read for all (a = all)
$ chmod u=rwx,g=rx,o=r file.txt # Set all at once

# Numeric mode
$ chmod 755 script.sh           # rwxr-xr-x
$ chmod 644 document.txt        # rw-r--r--
$ chmod 600 secret.key          # rw-------
$ chmod 700 private_dir         # rwx------

# Recursive
$ chmod -R 755 directory/       # Apply to all contents
$ chmod -R u+w directory/       # Add write for owner recursively

Changing Ownership with chown

# Change owner
$ sudo chown newowner file.txt

# Change owner and group
$ sudo chown newowner:newgroup file.txt

# Change only group
$ sudo chown :newgroup file.txt
$ chgrp newgroup file.txt       # Alternative

# Recursive
$ sudo chown -R david:staff directory/

# Follow symlinks
$ sudo chown -H david symlink   # Affect target of symlink

# Don't follow symlinks
$ sudo chown -h david symlink   # Affect symlink itself

Special Permission Bits

macOS supports the special Unix permission bits:

# setuid (4xxx) - Run as file owner
$ ls -l /usr/bin/sudo
-r-s--x--x  1 root  wheel  378848 Jan  1 00:00 /usr/bin/sudo
#   ^-- 's' indicates setuid

# setgid (2xxx) - Run as file group / inherit directory group
$ chmod 2755 shared_dir/

# Sticky bit (1xxx) - Only owner can delete files in directory
$ ls -ld /tmp
drwxrwxrwt  12 root  wheel  384 Jan 15 10:00 /tmp
#        ^-- 't' indicates sticky bit

# Set sticky bit
$ chmod 1777 shared_dir/
$ chmod +t shared_dir/

Default Permissions: umask

The umask determines default permissions for new files:

# View current umask
$ umask
022

# What this means:
# Files: 666 - 022 = 644 (rw-r--r--)
# Dirs:  777 - 022 = 755 (rwxr-xr-x)

# More restrictive umask
$ umask 077
# Files: 666 - 077 = 600 (rw-------)
# Dirs:  777 - 077 = 700 (rwx------)

# Set in shell profile (~/.zshrc)
umask 022

Access Control Lists (ACLs)

ACLs provide fine-grained permissions beyond the user/group/other model. macOS uses NFSv4-style ACLs.

Viewing ACLs

# View ACLs with ls
$ ls -le ~/Documents
total 0
drwx------+ 3 david  staff  96 Jan 15 10:00 Projects
 0: group:everyone deny delete

# More detailed view
$ ls -le document.txt
-rw-r--r--+ 1 david  staff  1024 Jan 15 10:00 document.txt
 0: user:alice allow read
 1: group:developers allow read,write
 2: group:everyone deny write

ACL entries are numbered and evaluated in order.

ACL Entry Format

Each ACL entry has:

<type>:<name> <action> <permissions>

Types:

  • user:username - Specific user
  • group:groupname - Specific group

Actions:

  • allow - Grant permission
  • deny - Explicitly deny permission

Permissions for files:

PermissionMeaning
readRead file contents
writeModify file contents
executeExecute file
deleteDelete file
appendAppend to file
readattrRead attributes
writeattrWrite attributes
readextattrRead extended attributes
writeextattrWrite extended attributes
readsecurityRead ACL
writesecurityModify ACL
chownChange ownership

Permissions for directories:

PermissionMeaning
listList directory contents
searchAccess files in directory
add_fileCreate files
add_subdirectoryCreate subdirectories
delete_childDelete items in directory
readattrRead attributes
writeattrWrite attributes
readextattrRead extended attributes
writeextattrWrite extended attributes
readsecurityRead ACL
writesecurityModify ACL
chownChange ownership

Adding ACL Entries

# Grant read access to specific user
$ chmod +a "user:alice allow read" document.txt

# Grant read/write to a group
$ chmod +a "group:developers allow read,write" project/

# Deny write to everyone
$ chmod +a "group:everyone deny write" readonly.txt

# Add ACL at specific position (0 = first)
$ chmod +a# 0 "user:bob deny write" file.txt

# Grant full control
$ chmod +a "user:admin allow read,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown" file.txt

Inheritance (Directories)

Directory ACLs can be inherited by new files:

# Add inherited ACL for files
$ chmod +a "group:developers allow read,write,file_inherit" project/

# Add inherited ACL for directories
$ chmod +a "group:developers allow read,write,execute,directory_inherit" project/

# Both inheritance types
$ chmod +a "group:developers allow read,write,file_inherit,directory_inherit" project/

# Limit inheritance to one level
$ chmod +a "group:developers allow read,write,file_inherit,limit_inherit" project/

Inheritance flags:

FlagMeaning
file_inheritApply to new files
directory_inheritApply to new subdirectories
limit_inheritDon’t propagate beyond direct children
only_inheritDon’t apply to directory itself, only children

Modifying ACL Entries

# View current ACLs
$ ls -le file.txt
-rw-r--r--+ 1 david  staff  1024 Jan 15 10:00 file.txt
 0: user:alice allow read
 1: group:developers allow read,write

# Remove specific entry by index
$ chmod -a# 0 file.txt

# Remove entry by content
$ chmod -a "user:alice allow read" file.txt

# Remove all ACLs
$ chmod -N file.txt

# Replace an entry at position
$ chmod =a# 0 "user:alice allow read,write" file.txt

Reordering ACLs

Order matters. Entries are evaluated first to last, and first match wins:

# Current order (deny evaluated before allow)
$ ls -le file.txt
 0: group:everyone deny write
 1: user:alice allow write

# Alice CANNOT write because deny comes first

# Reorder to allow Alice
$ chmod -a "group:everyone deny write" file.txt
$ chmod +a "user:alice allow write" file.txt
$ chmod +a "group:everyone deny write" file.txt

# New order
$ ls -le file.txt
 0: user:alice allow write
 1: group:everyone deny write

# Now Alice CAN write

Recursive ACL Operations

# Add ACL recursively
$ chmod -R +a "group:developers allow read" project/

# Remove all ACLs recursively
$ chmod -RN project/

Common Permission Patterns

Shared Project Directory

# Create shared directory
$ mkdir /Users/Shared/project
$ sudo chown :developers /Users/Shared/project
$ sudo chmod 2775 /Users/Shared/project

# Add ACL for team access with inheritance
$ sudo chmod +a "group:developers allow read,write,execute,delete,add_file,add_subdirectory,delete_child,file_inherit,directory_inherit" /Users/Shared/project

Read-Only for Most, Write for Few

# Base permissions: readable by all
$ chmod 644 document.txt

# ACL: specific users can write
$ chmod +a "user:editor allow write" document.txt
$ chmod +a "group:admins allow write" document.txt

Private User Directories

# Standard macOS home directory permissions
$ ls -ld ~
drwxr-x---+ 65 david  staff  2080 Jan 15 10:00 /Users/david

# ACL allows group access (for sharing)
$ ls -le ~
 0: group:everyone deny delete

Web Server Content

# Web-accessible but protected
$ sudo chown -R www:www /var/www/html
$ sudo chmod -R 755 /var/www/html
$ sudo find /var/www/html -type f -exec chmod 644 {} \;

# Writable upload directory
$ sudo chmod 775 /var/www/html/uploads
$ sudo chmod +a "group:www allow write,add_file,delete_child" /var/www/html/uploads

Troubleshooting Permissions

Permission Denied

# Check file permissions
$ ls -la file.txt
-rw------- 1 root wheel 0 Jan 15 10:00 file.txt

# Check ACLs
$ ls -le file.txt

# Check your effective UID/GID
$ id
uid=501(david) gid=20(staff)

# Check if SIP is blocking
$ ls -lO file.txt
-rw-r--r--  1 root  wheel  restricted file.txt
#                          ^-- restricted flag = SIP protected

Resetting Permissions

# Reset to standard file permissions
$ chmod 644 file.txt
$ chmod -N file.txt  # Remove ACLs

# Reset directory
$ chmod 755 directory/
$ chmod -RN directory/  # Remove all ACLs

# Reset home directory permissions
$ sudo diskutil resetUserPermissions / $(id -u)

Finding Permission Issues

# Find files you don't own
$ find /path -not -user $(whoami) 2>/dev/null

# Find files with unusual permissions
$ find /path -perm -002 -type f  # World-writable files
$ find /path -perm -4000 -type f  # Setuid files

# Find files with ACLs
$ ls -leR /path 2>/dev/null | grep -B1 "^[[:space:]]*[0-9]:"

Extended Attributes and Flags

macOS also uses extended attributes and file flags:

Extended Attributes

# View extended attributes
$ ls -l@ file.txt
-rw-r--r--@ 1 david  staff  1024 Jan 15 10:00 file.txt
    com.apple.quarantine	     57

# List all extended attributes
$ xattr file.txt
com.apple.quarantine

# View attribute value
$ xattr -p com.apple.quarantine file.txt
0083;5f123456;Safari;XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

# Remove quarantine attribute
$ xattr -d com.apple.quarantine file.txt

# Remove all extended attributes
$ xattr -c file.txt

File Flags (chflags)

# View flags
$ ls -lO /System
drwxr-xr-x  restricted /System

# Common flags
$ chflags hidden file.txt     # Hide from Finder
$ chflags nohidden file.txt   # Unhide

$ chflags uchg file.txt       # User immutable (can't modify)
$ chflags nouchg file.txt     # Remove immutable

$ sudo chflags schg file.txt  # System immutable (root only)
$ sudo chflags noschg file.txt

# The 'restricted' flag indicates SIP protection
# Cannot be changed without disabling SIP

Comparison with Linux

FeatureLinuxmacOS
Basic permissionsSameSame
View ACLsgetfaclls -le
Set ACLssetfaclchmod +a
ACL formatPOSIX ACLsNFSv4 ACLs
Extended attributesgetfattr/setfattrxattr
File flagschattrchflags
Mandatory accessSELinux, AppArmorSIP, Sandbox

Summary

macOS permissions combine multiple systems:

LayerToolPurpose
Unix permissionschmod, chownBasic access control
ACLschmod +a, ls -leFine-grained access
Extended attributesxattrMetadata (quarantine, etc.)
File flagschflagsSpecial protection
SIPcsrutilSystem protection

Key commands:

# View everything
$ ls -laeO@ file.txt

# Manage permissions
$ chmod 644 file.txt
$ chmod +a "user:alice allow read" file.txt
$ chmod -N file.txt

# Manage ownership
$ sudo chown user:group file.txt

# Manage attributes
$ xattr -l file.txt
$ xattr -d com.apple.quarantine file.txt

# Manage flags
$ chflags hidden file.txt

Understanding all these layers ensures you can properly secure files and troubleshoot access problems on macOS.

System Integrity Protection (SIP)

System Integrity Protection, introduced in OS X El Capitan (10.11), is macOS’s kernel-level protection mechanism that prevents even root from modifying critical system files. Understanding SIP is essential for any macOS administrator, especially when troubleshooting software that seems to inexplicably fail despite having proper permissions.

What SIP Protects

SIP protects several categories of system resources:

Protected Directories

# These directories are protected by SIP
/System/
/usr/          # Except /usr/local/
/bin/
/sbin/
/var/          # Some subdirectories

# These are NOT protected
/usr/local/
/Applications/
/Library/
~/

Even as root, you cannot modify protected paths:

$ sudo touch /System/test
touch: /System/test: Operation not permitted

$ sudo rm /bin/ls
rm: /bin/ls: Operation not permitted

$ sudo mv /usr/bin/zip /usr/bin/zip.bak
mv: rename /usr/bin/zip to /usr/bin/zip.bak: Operation not permitted

Protected System Processes

SIP prevents:

  • Attaching debuggers to system processes
  • Modifying running system processes
  • Loading unsigned kernel extensions (kexts)
  • Injecting code into protected processes
# Cannot attach to system processes
$ sudo lldb -p $(pgrep Finder)
error: attach failed: attach failed (Not allowed to attach to process.
Look in the console messages (Console.app) for possible reason.)

Kernel Extension Restrictions

# View loaded kernel extensions
$ kextstat | head -5
Index Refs Address            Size       Wired      Name (Version)
    1  148 0                  0          0          com.apple.kpi.bsd (23.2.0)
    2   18 0                  0          0          com.apple.kpi.dsep (23.2.0)
    3  181 0                  0          0          com.apple.kpi.iokit (23.2.0)
    4    0 0                  0          0          com.apple.kpi.kasan (23.2.0)

# Loading unsigned kexts is blocked by SIP
$ sudo kextload /path/to/unsigned.kext
/path/to/unsigned.kext failed to load - (libkern/kext) not loadable...

The Restricted Flag

Protected files carry a special restricted flag:

# View restricted flag
$ ls -lO /bin/ls
-rwxr-xr-x  1 root  wheel  restricted,compressed 134800 Jan  1 00:00 /bin/ls

$ ls -lO /System
drwxr-xr-x  6 root  wheel  restricted 192 Jan  1 00:00 /System

# Your files don't have this flag
$ ls -lO ~/Desktop
drwx------+ 5 david  staff  - 160 Jan 15 10:00 /Users/david/Desktop

Checking SIP Status

# Check if SIP is enabled
$ csrutil status
System Integrity Protection status: enabled.

# On systems with SIP disabled
$ csrutil status
System Integrity Protection status: disabled.

# Check authenticated-root status (macOS 11+)
$ csrutil authenticated-root status
Authenticated Root status: enabled

What SIP Prevents You From Doing

System Modifications

# Cannot modify system binaries
$ sudo vim /usr/bin/python3
E: Unable to write file

# Cannot create files in protected directories
$ sudo mkdir /System/mydir
mkdir: /System/mydir: Operation not permitted

# Cannot modify boot configuration
$ sudo nvram boot-args="some value"
nvram: Error setting variable - 'boot-args': (iokit/common) not permitted

Code Injection

# Cannot inject into protected processes
$ sudo DYLD_INSERT_LIBRARIES=/path/to/lib.dylib /System/Applications/Finder.app/Contents/MacOS/Finder
# DYLD_INSERT_LIBRARIES is ignored for protected binaries

Kernel Extension Loading

# Cannot load unsigned kexts
$ sudo kextload unsigned.kext
# Blocked by SIP

# Cannot modify kext cache
$ sudo touch /System/Library/Extensions
touch: /System/Library/Extensions: Operation not permitted

When You Might Need to Disable SIP

Disabling SIP should be rare and temporary. Valid reasons include:

  1. Installing specific kernel extensions (legacy drivers)
  2. Security research (analyzing malware, reverse engineering)
  3. Modifying system behavior for development
  4. Recovering from system issues where SIP is in the way

Invalid reasons (there’s usually a better way):

  • Installing software that wants to modify /usr/bin
  • General “I want full control” sentiment
  • A Stack Overflow answer told you to

How to Disable SIP

SIP can only be disabled from Recovery Mode:

Intel Macs

  1. Restart your Mac
  2. Hold Command + R during startup to boot into Recovery Mode
  3. From the menu bar, select Utilities > Terminal
  4. Disable SIP:
# Disable all SIP protections
$ csrutil disable
Successfully disabled System Integrity Protection. Please restart the machine for the changes to take effect.

# Or disable specific protections only
$ csrutil enable --without kext      # Allow unsigned kexts
$ csrutil enable --without fs        # Allow filesystem modifications
$ csrutil enable --without debug     # Allow debugging system processes
$ csrutil enable --without nvram     # Allow NVRAM modifications
  1. Restart: reboot

Apple Silicon Macs

  1. Shut down your Mac completely
  2. Press and hold the Power button until “Loading startup options” appears
  3. Click Options, then Continue
  4. If prompted, select a user and enter their password
  5. From the menu bar, select Utilities > Terminal
  6. Disable SIP:
$ csrutil disable

Note: Apple Silicon Macs have additional security considerations. You may also need to:

# Allow kernel extensions from identified developers
$ spctl kext-consent add <TEAM-ID>

# Change security policy (Startup Security Utility)
# Options: Full Security, Reduced Security, Permissive Security
  1. Restart

Re-enabling SIP

Always re-enable SIP when you’re done:

  1. Boot into Recovery Mode (same method as above)
  2. Open Terminal
  3. Enable SIP:
$ csrutil enable
Successfully enabled System Integrity Protection. Please restart the machine for the changes to take effect.
  1. Restart

Partial SIP Configuration

You can selectively disable SIP features:

# From Recovery Mode Terminal:

# See all options
$ csrutil enable --help

# Common partial configurations:
$ csrutil enable --without kext        # Kernel extensions only
$ csrutil enable --without fs          # Filesystem protection only
$ csrutil enable --without debug       # Process debugging only
$ csrutil enable --without nvram       # NVRAM protection only
$ csrutil enable --without dtrace      # DTrace restrictions only

# Multiple options
$ csrutil enable --without kext --without debug

# Check current configuration
$ csrutil status
System Integrity Protection status: enabled (Custom Configuration).

Configuration:
    Apple Internal: disabled
    Kext Signing: disabled
    Filesystem Protections: enabled
    Debugging Restrictions: enabled
    DTrace Restrictions: enabled
    NVRAM Protections: enabled
    BaseSystem Verification: enabled

SIP and Development

DTrace Limitations

With SIP enabled, DTrace cannot instrument protected processes:

# This works (your own process)
$ sudo dtrace -n 'syscall:::entry { @[execname] = count(); }'

# This won't show system process details
# Many probes are restricted

Debugging Limitations

# Cannot debug protected processes
$ sudo lldb -n Finder
error: attach failed: attach failed (Not allowed to attach to process.)

# Can debug your own processes
$ lldb ./myapp
(lldb) target create "./myapp"
Current executable set to './myapp' (arm64).

Building Software

Most development works fine with SIP enabled:

# /usr/local is not protected - Homebrew works fine
$ brew install wget

# /Applications is not protected
$ cp -r MyApp.app /Applications/

# Your home directory is not protected
$ make install PREFIX=$HOME/local

SIP Bypass Attempts (Don’t Do This)

Historical SIP bypasses have been patched:

  • Exploiting installer packages (fixed)
  • Using csrutil outside Recovery Mode (never worked properly)
  • Kernel vulnerability exploitation (patched)

Attempting to bypass SIP:

  • May indicate malware
  • Voids any support from Apple
  • Can brick your system
  • Will be patched in future updates

Authenticated Root (macOS 11+)

macOS Big Sur introduced Signed System Volume (SSV) and Authenticated Root:

# Check authenticated root status
$ csrutil authenticated-root status
Authenticated Root status: enabled

# The system volume is cryptographically sealed
$ diskutil list
...
/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +994.7 GB   disk3
   1:                APFS Volume Macintosh HD            14.9 GB    disk3s1
   2:              APFS Snapshot com.apple.os.update-... 14.9 GB    disk3s1s1
...

The system runs from a cryptographically verified snapshot. Even with SIP disabled, system modifications won’t persist across reboots without additional steps:

# To make system modifications persist (macOS 11+):
# 1. Disable SIP and authenticated-root
$ csrutil disable
$ csrutil authenticated-root disable

# 2. Make your modifications
# 3. Create a new snapshot
$ sudo bless --folder /Volumes/Macintosh\ HD/System/Library/CoreServices --bootefi --create-snapshot

# 4. Re-enable protections when done

Troubleshooting SIP Issues

Operation Not Permitted

$ sudo some-command
Operation not permitted

# Check if it's SIP-related
$ ls -lO /path/to/file
# Look for 'restricted' flag

# Or check csrutil
$ csrutil status

Third-Party Software Issues

Some software legitimately needs SIP disabled:

  • Legacy virtualization software
  • Certain security/analysis tools
  • Some kernel extension drivers

Check if the vendor has:

  1. A newer version that works with SIP
  2. A System Extension (modern kext replacement)
  3. Official guidance on SIP requirements

Kext Loading Failures

# Check why kext won't load
$ sudo kextutil -v /path/to/kext.kext
Kext rejected due to system policy

# Check kext consent
$ sudo spctl kext-consent status
Kernel Extension User Consent: ENABLED

$ sudo spctl kext-consent list
ABCDE12345  # Allowed team IDs

Modern macOS prefers System Extensions over kexts:

# View system extensions
$ systemextensionsctl list
1 extension(s)
--- com.apple.system_extension.network_extension
enabled	active	teamID	bundleID (version)	name	[state]
*	*	ABC123	com.example.ext (1.0/1)	MyExtension	[activated enabled]

Best Practices

  1. Keep SIP enabled for daily use
  2. Use /usr/local for custom software
  3. Use Homebrew instead of modifying system paths
  4. If you must disable SIP:
    • Do it for the minimum time necessary
    • Disable only the specific protection needed
    • Re-enable immediately after
    • Document what you did and why
  5. Never disable SIP on production machines
  6. Consider virtualization for testing that requires SIP disabled

Summary

SIP is a fundamental security feature of modern macOS:

AspectDetails
PurposeProtect system integrity from malware and mistakes
Protected/System, /usr, /bin, /sbin, kernel, system processes
Unprotected/usr/local, /Applications, /Library, ~/
Check statuscsrutil status
DisableRecovery Mode only
Best practiceKeep enabled, use /usr/local

Key commands:

# Check status
$ csrutil status

# View restricted flag
$ ls -lO /System

# From Recovery Mode only:
$ csrutil disable
$ csrutil enable
$ csrutil enable --without kext

SIP may occasionally frustrate you, but it’s a crucial defense against both malware and accidental system damage. Work with it, not against it.

Configuring the pf Firewall

macOS uses pf (packet filter), the powerful firewall from OpenBSD. Unlike Linux’s iptables/nftables, pf uses a clean, readable configuration syntax. This chapter covers configuring pf for network security on macOS.

pf Overview

pf has been macOS’s built-in firewall since OS X 10.7 (Lion). It operates at the kernel level, filtering packets before they reach applications.

# Check if pf is enabled
$ sudo pfctl -s info
Status: Disabled                      # or Enabled
...

# Enable pf
$ sudo pfctl -e
pf enabled

# Disable pf
$ sudo pfctl -d
pf disabled

pf vs iptables

Coming from Linux, here’s how pf compares to iptables:

Featurepf (macOS)iptables (Linux)
SyntaxEnglish-likeCommand flags
Config file/etc/pf.conf/etc/iptables/rules.v4
Rule orderLast match winsFirst match wins
TablesBuilt-in supportipset (separate)
NATIntegratedSeparate chain
LoggingBuilt-in-j LOG target

Syntax comparison:

# iptables: Block incoming SSH
iptables -A INPUT -p tcp --dport 22 -j DROP

# pf: Block incoming SSH
block in proto tcp from any to any port 22

Configuration File: /etc/pf.conf

The main configuration file is /etc/pf.conf:

$ sudo cat /etc/pf.conf
#
# Default PF configuration file.
#
# This file contains the main ruleset, which gets automatically loaded
# at startup.  PF will not be automatically enabled, however.  Instead,
# each component which utilizes PF is responsible for enabling and disabling
# PF via -E and -X as documented in pfctl(8).  That will ensure that PF
# is disabled only when the last enable reference is released.
#
# Care must be taken to ensure that the main ruleset does not get flushed,
# as the nested anchors rely on the anchor point defined here. In
# particular, establish this anchor point first before flushing the main
# ruleset.
#

# Options
set block-policy drop
set fingerprints "/etc/pf.os"
set ruleset-optimization basic
set skip on lo0

# Normalization
scrub in all no-df

# Anchors
anchor "com.apple/*"
load anchor "com.apple" from "/etc/pf.anchors/com.apple"

Configuration Sections

A typical pf.conf has these sections:

  1. Macros (variables)
  2. Tables (IP address lists)
  3. Options (global settings)
  4. Scrub (packet normalization)
  5. NAT/Redirect (if needed)
  6. Filter rules (the actual firewall rules)

Basic Rule Syntax

Rule Structure

action [direction] [log] [quick] [on interface] [proto protocol]
       [from source] [to destination] [flags] [state]

Components:

  • action: block or pass
  • direction: in or out
  • log: Log matching packets
  • quick: Stop processing on match (first-match wins)
  • on interface: on en0, on lo0
  • proto: tcp, udp, icmp
  • from/to: Source and destination
  • port: Port number or name

Simple Examples

# Block all incoming traffic
block in all

# Pass all outgoing traffic
pass out all

# Block incoming SSH
block in proto tcp from any to any port 22

# Allow incoming HTTP and HTTPS
pass in proto tcp from any to any port { 80, 443 }

# Block specific IP
block in from 192.168.1.100

# Allow from specific network
pass in from 192.168.1.0/24

Creating a Custom Firewall

Let’s create a practical firewall configuration:

Step 1: Create Custom Config

$ sudo nano /etc/pf.custom.conf
# /etc/pf.custom.conf - Custom firewall rules

# === MACROS ===
ext_if = "en0"                          # External interface (Wi-Fi or Ethernet)
tcp_services = "{ 22, 80, 443 }"        # Allowed inbound TCP ports
udp_services = "{ 53, 123 }"            # Allowed inbound UDP ports

# Trusted networks
trusted_nets = "{ 192.168.1.0/24, 10.0.0.0/8 }"

# === TABLES ===
# Blocked hosts (can be modified at runtime)
table <blocklist> persist

# === OPTIONS ===
set block-policy drop                    # Silently drop blocked packets
set skip on lo0                          # Don't filter loopback

# === SCRUB ===
scrub in all                             # Normalize incoming packets

# === FILTER RULES ===

# Default deny incoming, allow outgoing
block in all
pass out all keep state

# Allow all traffic on loopback
pass quick on lo0 all

# Block hosts in blocklist
block in quick from <blocklist>

# Allow ICMP (ping)
pass in inet proto icmp all icmp-type { echoreq, unreach }

# Allow incoming SSH from trusted networks only
pass in on $ext_if proto tcp from $trusted_nets to any port 22

# Allow incoming web traffic
pass in on $ext_if proto tcp from any to any port { 80, 443 }

# Allow established connections
pass in on $ext_if proto tcp from any to any flags S/SA keep state

# Block and log everything else
block in log all

Step 2: Test the Configuration

# Check syntax without loading
$ sudo pfctl -n -f /etc/pf.custom.conf
# No output means no errors

# Verbose check
$ sudo pfctl -n -v -f /etc/pf.custom.conf
# Shows parsed rules

Step 3: Load the Configuration

# Load rules
$ sudo pfctl -f /etc/pf.custom.conf

# Enable pf
$ sudo pfctl -e
pf enabled

# Verify rules are loaded
$ sudo pfctl -s rules
block drop in all
pass out all flags S/SA keep state
pass quick on lo0 all flags S/SA keep state
block drop in quick from <blocklist> to any
pass in inet proto icmp all icmp-type echoreq keep state
pass in inet proto icmp all icmp-type unreach keep state
pass in on en0 proto tcp from 192.168.1.0/24 to any port = ssh flags S/SA keep state
pass in on en0 proto tcp from 10.0.0.0/8 to any port = ssh flags S/SA keep state
pass in on en0 proto tcp from any to any port = http flags S/SA keep state
pass in on en0 proto tcp from any to any port = https flags S/SA keep state
block drop in log all

Managing Tables

Tables are dynamic lists of IP addresses, perfect for blocklists:

# View table contents
$ sudo pfctl -t blocklist -T show
# (empty if no IPs added)

# Add IP to blocklist
$ sudo pfctl -t blocklist -T add 192.168.1.100
1/1 addresses added.

# Add multiple IPs
$ sudo pfctl -t blocklist -T add 10.0.0.1 10.0.0.2 10.0.0.3

# Add network
$ sudo pfctl -t blocklist -T add 172.16.0.0/16

# Remove IP from blocklist
$ sudo pfctl -t blocklist -T delete 192.168.1.100
1/1 addresses deleted.

# Flush entire table
$ sudo pfctl -t blocklist -T flush
0 addresses deleted.

# Load table from file
$ cat /etc/pf.blocklist.txt
# Bad actors
192.168.1.100
10.0.0.50
172.16.0.0/16

$ sudo pfctl -t blocklist -T replace -f /etc/pf.blocklist.txt

# Show table statistics
$ sudo pfctl -t blocklist -T show -v
   192.168.1.100
        Cleared:     Wed Jan 15 10:00:00 2024
        In/Block:    [ Packets: 0         Bytes: 0         ]
        In/Pass:     [ Packets: 0         Bytes: 0         ]
        Out/Block:   [ Packets: 0         Bytes: 0         ]
        Out/Pass:    [ Packets: 0         Bytes: 0         ]

Viewing Firewall Status

# General info
$ sudo pfctl -s info
Status: Enabled for 0 days 05:30:22    Debug: Urgent

State Table                          Total             Rate
  current entries                       45
  searches                          123456           6.2/s
  inserts                            12345           0.6/s
  removals                           12300           0.6/s
Counters
  match                              67890           3.4/s
  bad-offset                             0           0.0/s
  fragment                               0           0.0/s
  ...

# View all rules
$ sudo pfctl -s rules

# View rules with stats
$ sudo pfctl -s rules -v
@0 block drop in all
  [ Evaluations: 50000     Packets: 1234      Bytes: 98765     States: 0     ]
@1 pass out all flags S/SA keep state
  [ Evaluations: 45000     Packets: 40000     Bytes: 5000000   States: 45    ]
...

# View state table (active connections)
$ sudo pfctl -s state
all tcp 192.168.1.100:52345 -> 93.184.216.34:443       ESTABLISHED:ESTABLISHED
all tcp 192.168.1.100:52346 -> 93.184.216.34:443       ESTABLISHED:ESTABLISHED
...

# View NAT rules
$ sudo pfctl -s nat

# View all (rules, nat, tables, etc.)
$ sudo pfctl -s all

Logging

pf logging uses pflog:

# Enable logging interface
$ sudo ifconfig pflog0 create
$ sudo ifconfig pflog0 up

# View logged packets in real-time
$ sudo tcpdump -n -e -ttt -i pflog0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on pflog0, link-type PFLOG (OpenBSD pflog file), capture size 262144 bytes
 00:00:00.000000 rule 10/0(match): block in on en0: 192.168.1.50.54321 > 192.168.1.100.22: Flags [S], seq 123456789, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 123456 ecr 0,sackOK,eol], length 0

# Log to file
$ sudo tcpdump -n -e -ttt -i pflog0 -w /var/log/pflog.pcap

Add logging to rules:

# Log blocked packets
block in log all

# Log specific rule
pass in log on en0 proto tcp from any to any port 22

# Log all with extra detail
block in log (all) from any to any

Making Rules Persistent

Method 1: Modify /etc/pf.conf

Edit the default file (be careful with Apple’s anchors):

$ sudo cp /etc/pf.conf /etc/pf.conf.backup
$ sudo nano /etc/pf.conf

# Add your rules at the end
# ... (keep existing Apple anchors)

# Custom rules
block in proto tcp from any to any port 23    # Block telnet
pass in proto tcp from any to any port { 80, 443 }

Create a custom anchor:

# Create custom rules file
$ sudo nano /etc/pf.anchors/custom

# Contents of /etc/pf.anchors/custom:
# Custom firewall rules
table <blocklist> persist file "/etc/pf.blocklist.txt"
block in quick from <blocklist>
pass in proto tcp from any to any port { 80, 443 }

Add anchor to main config:

$ sudo nano /etc/pf.conf

# Add after existing anchors:
anchor "custom"
load anchor "custom" from "/etc/pf.anchors/custom"

Method 3: Launch Daemon

Create a launch daemon to load pf at boot:

$ sudo nano /Library/LaunchDaemons/com.custom.pfctl.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.custom.pfctl</string>
    <key>Program</key>
    <string>/sbin/pfctl</string>
    <key>ProgramArguments</key>
    <array>
        <string>/sbin/pfctl</string>
        <string>-e</string>
        <string>-f</string>
        <string>/etc/pf.custom.conf</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

Load the daemon:

$ sudo launchctl load /Library/LaunchDaemons/com.custom.pfctl.plist

Application Firewall vs pf

macOS has two firewalls:

  1. Application Firewall (socketfilterfw) - GUI-configurable, app-based
  2. pf - Network-level, rule-based
# Application Firewall status
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
Firewall is enabled. (State = 1)

# Enable Application Firewall
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on

# Block all incoming (stealth mode)
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setblockall on

# Allow specific app
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /Applications/MyApp.app

# List rules
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --listapps

Both firewalls can be used together. pf operates at a lower level.

Common Configurations

Web Server

# /etc/pf.anchors/webserver
set skip on lo0

# Default deny
block in all
pass out all keep state

# Allow web traffic
pass in proto tcp from any to any port { 80, 443 } keep state

# Allow SSH from admin network
pass in proto tcp from 10.0.0.0/8 to any port 22 keep state

# Allow ICMP
pass inet proto icmp icmp-type echoreq keep state

Development Machine

# /etc/pf.anchors/devmachine
set skip on lo0

# Default deny
block in all
pass out all keep state

# Allow local network
pass in from 192.168.1.0/24 keep state

# Allow common dev ports
pass in proto tcp from any to any port { 3000, 4000, 5000, 8000, 8080 } keep state

# Allow AirDrop/Bonjour
pass in proto udp from any to any port { 5353 } keep state

Strict Workstation

# /etc/pf.anchors/strict
set skip on lo0

table <blocklist> persist file "/etc/pf.blocklist.txt"

# Block known bad actors
block in quick from <blocklist>
block out quick to <blocklist>

# Default policies
block in all
pass out all keep state

# No incoming connections at all
# All outgoing traffic allowed

Troubleshooting

Rules Not Working

# Verify pf is enabled
$ sudo pfctl -s info | grep Status
Status: Enabled

# Check rule order (last match wins!)
$ sudo pfctl -s rules

# Test specific rule
$ sudo pfctl -s rules -v | grep -A1 "port = ssh"

# Flush and reload
$ sudo pfctl -F all
$ sudo pfctl -f /etc/pf.conf

Cannot Connect After Rule Change

# Disable pf temporarily
$ sudo pfctl -d

# Or flush rules
$ sudo pfctl -F rules

# Fix your configuration
$ sudo nano /etc/pf.conf

# Reload
$ sudo pfctl -f /etc/pf.conf
$ sudo pfctl -e

Viewing Dropped Packets

# Enable logging on drop rule
# In pf.conf: block in log all

# Watch pflog
$ sudo tcpdump -i pflog0 -n

# Or check statistics
$ sudo pfctl -s info | grep -A5 Counters

Summary

pf is a powerful, BSD-style firewall available on macOS:

TaskCommand
Enablesudo pfctl -e
Disablesudo pfctl -d
Load rulessudo pfctl -f /etc/pf.conf
Show rulessudo pfctl -s rules
Show statesudo pfctl -s state
Add to tablesudo pfctl -t name -T add IP
Flush rulessudo pfctl -F rules
Test configsudo pfctl -n -f /etc/pf.conf

Key concepts:

  • Last match wins (unlike iptables)
  • quick keyword makes it first-match
  • Tables for dynamic IP lists
  • Anchors for modular configuration
  • State tracking with keep state

pf provides enterprise-grade firewall capabilities with a clean, readable syntax. Master it, and you’ll have complete control over your Mac’s network security.

Unified Logging System

macOS 10.12 (Sierra) introduced Unified Logging, replacing the traditional Unix syslog system. All system and application logs now flow through a single, efficient logging mechanism. Understanding this system is essential for troubleshooting macOS issues.

Unified Logging vs Traditional Syslog

Traditional Unix logging had limitations:

AspectTraditional SyslogUnified Logging
StoragePlain text filesCompressed binary database
PerformanceDisk I/O intensiveMemory-buffered, compressed
Querygrep through filesStructured queries
MetadataLimitedRich (subsystem, category, type)
PrivacyNoneBuilt-in privacy controls
Location/var/log//var/db/diagnostics/
# Old way (some logs still here)
$ ls /var/log/
asl/
com.apple.xpc.launchd/
install.log
system.log
wifi.log

# New way
$ ls /var/db/diagnostics/
Persist/
Special/
timesync/

The log Command

The primary tool for unified logging is the log command:

# Basic usage
$ log show                    # Show recent logs
$ log stream                  # Follow logs in real-time
$ log collect                 # Gather logs for analysis

Real-Time Log Streaming

# Stream all logs (very verbose)
$ log stream
Timestamp                       Thread     Type        Activity             PID    TTL
2024-01-15 10:00:00.123456-0800 0x1234     Default     0x0                  123    0    kernel: (AppleUSBXHCI) ...
2024-01-15 10:00:00.234567-0800 0x2345     Info        0x0                  456    0    mds: (Spotlight) ...
...

# Stream with level filter
$ log stream --level error

# Stream for specific process
$ log stream --process Safari

# Stream for specific subsystem
$ log stream --predicate 'subsystem == "com.apple.network"'

# Stream with debug messages (normally hidden)
$ log stream --level debug

# Stream with info level and above
$ log stream --level info

Viewing Historical Logs

# Show logs from last hour
$ log show --last 1h

# Show logs from last 30 minutes
$ log show --last 30m

# Show logs from specific time range
$ log show --start "2024-01-15 09:00:00" --end "2024-01-15 10:00:00"

# Show logs from today
$ log show --start "$(date '+%Y-%m-%d') 00:00:00"

# Show logs since boot
$ log show --start "$(sysctl -n kern.boottime | awk '{print $4}' | tr -d ',')"

Filtering with Predicates

Predicates are the key to finding relevant logs. They use NSPredicate syntax:

Basic Predicate Examples

# Filter by process name
$ log show --predicate 'process == "Safari"' --last 1h

# Filter by subsystem
$ log show --predicate 'subsystem == "com.apple.wifi"' --last 1h

# Filter by category
$ log show --predicate 'category == "connection"' --last 1h

# Filter by message content
$ log show --predicate 'eventMessage contains "error"' --last 1h

# Case-insensitive search
$ log show --predicate 'eventMessage contains[c] "ERROR"' --last 1h

# Filter by log type/level
$ log show --predicate 'messageType == error' --last 1h
$ log show --predicate 'messageType == fault' --last 1h

Combining Predicates

# AND condition
$ log show --predicate 'process == "kernel" AND eventMessage contains "USB"' --last 1h

# OR condition
$ log show --predicate 'process == "Safari" OR process == "WebKit"' --last 1h

# Complex combinations
$ log show --predicate '(subsystem == "com.apple.network" OR subsystem == "com.apple.wifi") AND messageType == error' --last 1h

# NOT condition
$ log show --predicate 'NOT process == "kernel"' --last 1h

# Multiple conditions
$ log show --predicate 'subsystem BEGINSWITH "com.apple" AND category == "default" AND eventMessage contains[c] "fail"' --last 1h

Predicate Operators

OperatorMeaning
==Equals
!=Not equals
<, >, <=, >=Comparisons
containsString contains
BEGINSWITHString starts with
ENDSWITHString ends with
MATCHESRegular expression
AND, OR, NOTLogical operators
[c]Case-insensitive modifier

Available Predicate Fields

FieldDescription
processProcess name
processIDProcess ID (PID)
subsystemLogging subsystem
categoryLog category
eventMessageThe log message
messageTypeLog level (default, info, debug, error, fault)
senderImagePathPath to the binary that logged
eventTypeactivityCreateEvent, logEvent, etc.

Log Levels and Types

Unified logging has five log types:

TypeDescriptionWhen to Use
DefaultStandard messagesGeneral information
InfoInformationalHelpful but verbose
DebugDebuggingDevelopment only
ErrorErrorsSomething went wrong
FaultCriticalSystem/app failure
# Show only errors and faults
$ log show --predicate 'messageType == error OR messageType == fault' --last 1h

# Show debug messages (normally hidden, requires debug profile)
$ log show --level debug --predicate 'process == "myapp"' --last 1h

By default, Debug and Info messages may be hidden. Enable them:

# Enable debug logging for a subsystem (persistent)
$ sudo log config --mode "persist:debug" --subsystem com.example.myapp

# Enable for current boot only
$ sudo log config --mode "level:debug" --subsystem com.example.myapp

# Reset to default
$ sudo log config --mode "persist:default" --subsystem com.example.myapp

# Check current configuration
$ sudo log config --status --subsystem com.example.myapp

Practical Examples

Troubleshooting Wi-Fi

# Stream Wi-Fi logs
$ log stream --predicate 'subsystem == "com.apple.wifi"' --level debug

# Show recent Wi-Fi errors
$ log show --predicate 'subsystem == "com.apple.wifi" AND messageType >= error' --last 1h

# Find Wi-Fi disconnection reasons
$ log show --predicate 'subsystem == "com.apple.wifi" AND eventMessage contains "disconnect"' --last 4h

Investigating Crashes

# Find crash logs
$ log show --predicate 'eventMessage contains "crash" OR process == "ReportCrash"' --last 1h

# Application-specific crashes
$ log show --predicate 'process == "Safari" AND (eventMessage contains "crash" OR messageType == fault)' --last 1h

# Kernel panics
$ log show --predicate 'process == "kernel" AND messageType == fault' --last 24h

Login/Authentication Issues

# Authentication logs
$ log show --predicate 'subsystem == "com.apple.opendirectoryd" OR subsystem == "com.apple.securityd"' --last 1h

# Login events
$ log show --predicate 'eventMessage contains "login" OR eventMessage contains "authentication"' --last 1h

# SSH connections
$ log show --predicate 'process == "sshd"' --last 1h

Disk and Storage

# Disk-related messages
$ log show --predicate 'subsystem contains "disk" OR process == "diskutil" OR process == "fsck"' --last 1h

# APFS operations
$ log show --predicate 'subsystem == "com.apple.apfs"' --last 1h

# Time Machine
$ log show --predicate 'subsystem == "com.apple.TimeMachine"' --last 4h

Network Issues

# Network subsystem
$ log show --predicate 'subsystem == "com.apple.network"' --last 30m

# DNS issues
$ log show --predicate 'eventMessage contains "DNS" OR process == "mDNSResponder"' --last 1h

# VPN connections
$ log show --predicate 'subsystem == "com.apple.networkextension"' --last 1h

Application Debugging

# Specific application
$ log show --predicate 'process == "MyApp"' --last 1h

# Application launch issues
$ log show --predicate 'eventMessage contains "MyApp" AND (eventMessage contains "launch" OR eventMessage contains "terminate")' --last 1h

# launchd service issues
$ log show --predicate 'subsystem == "com.apple.launchd" AND eventMessage contains "com.example"' --last 1h

Output Formatting

Format Options

# Default format
$ log show --last 5m

# JSON format (for scripting)
$ log show --last 5m --style json

# Compact format
$ log show --last 5m --style compact

# Syslog-like format
$ log show --last 5m --style syslog

# Include more information
$ log show --last 5m --info --debug

JSON Output for Scripting

# Get JSON output
$ log show --predicate 'process == "Safari"' --last 5m --style json

# Parse with jq
$ log show --predicate 'messageType == error' --last 1h --style json | jq '.[].eventMessage'

# Count errors by process
$ log show --predicate 'messageType == error' --last 1h --style json | jq 'group_by(.processImagePath) | map({process: .[0].processImagePath, count: length}) | sort_by(.count) | reverse'

Collecting Logs

For support or analysis, collect logs into a file:

# Collect logs to archive
$ sudo log collect --last 1d --output ~/Desktop/logs.logarchive

# Collect with specific time range
$ sudo log collect --start "2024-01-15 09:00:00" --end "2024-01-15 12:00:00" --output ~/Desktop/incident.logarchive

# Open in Console.app
$ open ~/Desktop/logs.logarchive

The .logarchive format can be opened in Console.app or queried with log show:

# Query collected logs
$ log show --archive ~/Desktop/logs.logarchive --predicate 'messageType == error'

Legacy Log Files

Some traditional log files still exist:

# Install log
$ cat /var/log/install.log

# System log (limited)
$ cat /var/log/system.log

# Wi-Fi log
$ cat /var/log/wifi.log

# Apache (if enabled)
$ cat /var/log/apache2/error_log

# Application-specific
$ cat ~/Library/Logs/DiagnosticReports/*.crash

Console.app

Console.app provides a GUI for unified logging:

# Open Console
$ open -a Console

# Open with specific log archive
$ open ~/Desktop/logs.logarchive

Console.app features:

  • Real-time streaming
  • Predicate builder
  • Log favorites
  • Device logs (iOS devices connected)

Common Subsystems Reference

SubsystemDescription
com.apple.wifiWi-Fi
com.apple.networkNetworking
com.apple.networkextensionVPN, network extensions
com.apple.apfsAPFS filesystem
com.apple.launchdlaunchd services
com.apple.securitydSecurity daemon
com.apple.opendirectorydDirectory services
com.apple.TimeMachineTime Machine
com.apple.SpotlightSpotlight indexing
com.apple.xpcXPC services
com.apple.coredataCore Data
com.apple.bluetoothBluetooth
com.apple.audioAudio
com.apple.powerdPower management

Performance Considerations

Unified logging is designed for efficiency, but verbose queries can impact performance:

# Expensive: search all logs
$ log show --last 24h  # Can be slow

# Better: use predicates to filter
$ log show --predicate 'process == "Safari"' --last 24h

# Best: combine specific predicates
$ log show --predicate 'process == "Safari" AND messageType == error' --last 24h

Summary

Unified logging replaces traditional syslog with a modern, efficient system:

TaskCommand
Stream live logslog stream
Show historical logslog show --last 1h
Filter by process--predicate 'process == "name"'
Filter by subsystem--predicate 'subsystem == "com.apple.x"'
Filter by message--predicate 'eventMessage contains "text"'
Show only errors--predicate 'messageType == error'
Collect logssudo log collect --output file.logarchive
JSON output--style json

Key predicates to remember:

# Process-based
process == "ProcessName"

# Subsystem-based
subsystem == "com.apple.something"

# Message content
eventMessage contains "search term"
eventMessage contains[c] "case insensitive"

# Log level
messageType == error
messageType == fault

# Combined
process == "Safari" AND messageType == error

Master the log command, and you’ll be able to diagnose almost any macOS issue from the command line.

System Configuration via defaults

The defaults command is macOS’s tool for reading and writing user preferences. Every application preference, system setting, and hidden configuration option is stored in property list (plist) files, and defaults gives you direct access to them. Mastering this command unlocks powerful system customization.

How Preferences Work

macOS stores preferences in plist files:

# User preferences
~/Library/Preferences/

# System-wide preferences (require sudo)
/Library/Preferences/

# Current host preferences (hardware-specific)
~/Library/Preferences/ByHost/

Each application has a domain (usually its bundle identifier):

# Finder preferences
~/Library/Preferences/com.apple.finder.plist

# Safari preferences
~/Library/Preferences/com.apple.Safari.plist

# Global preferences
~/Library/Preferences/.GlobalPreferences.plist
# Also accessible as NSGlobalDomain

Basic Usage

Reading Preferences

# Read all preferences for an app
$ defaults read com.apple.finder
{
    AppleShowAllExtensions = 1;
    AppleShowAllFiles = 1;
    CreateDesktop = 1;
    ...
}

# Read specific key
$ defaults read com.apple.finder AppleShowAllFiles
1

# Read from global domain
$ defaults read NSGlobalDomain AppleShowAllExtensions
1

# Read a specific plist file
$ defaults read ~/Library/Preferences/com.apple.finder.plist

# List all domains
$ defaults domains
com.apple.AMPDevicesAgent, com.apple.AMPLibraryAgent, com.apple.AddressBook, ...

# Find domains containing "apple"
$ defaults domains | tr ',' '\n' | grep -i apple

Writing Preferences

# Write a boolean value
$ defaults write com.apple.finder AppleShowAllFiles -bool true

# Write an integer
$ defaults write com.apple.dock autohide-delay -int 0

# Write a float
$ defaults write com.apple.dock autohide-time-modifier -float 0.5

# Write a string
$ defaults write com.apple.finder NewWindowTarget -string "PfLo"

# Write an array
$ defaults write com.apple.dock persistent-apps -array

# Write to global domain
$ defaults write NSGlobalDomain AppleShowAllExtensions -bool true

# Write to current host (hardware-specific)
$ defaults -currentHost write com.apple.screensaver idleTime -int 300

Deleting Preferences

# Delete a specific key (revert to default)
$ defaults delete com.apple.finder AppleShowAllFiles

# Delete entire domain (dangerous!)
$ defaults delete com.apple.finder

# Be careful - there's no undo!

Data Types

The defaults command supports these data types:

TypeFlagExample
Boolean-booltrue, false, yes, no, 1, 0
Integer-int42, 0, -1
Float-float0.5, 1.0, 3.14
String-string"Hello"
Array-arrayMultiple values
Dict-dictKey-value pairs
Data-dataHex-encoded data
Date-dateISO 8601 format

Essential Finder Customizations

# Show hidden files
$ defaults write com.apple.finder AppleShowAllFiles -bool true
$ killall Finder

# Show all file extensions
$ defaults write NSGlobalDomain AppleShowAllExtensions -bool true
$ killall Finder

# Show path bar
$ defaults write com.apple.finder ShowPathbar -bool true

# Show status bar
$ defaults write com.apple.finder ShowStatusBar -bool true

# Show full POSIX path in title bar
$ defaults write com.apple.finder _FXShowPosixPathInTitle -bool true

# Keep folders on top when sorting by name
$ defaults write com.apple.finder _FXSortFoldersFirst -bool true

# Default to list view
$ defaults write com.apple.finder FXPreferredViewStyle -string "Nlsv"
# Nlsv = List, icnv = Icon, clmv = Column, glyv = Gallery

# Disable warning when changing file extension
$ defaults write com.apple.finder FXEnableExtensionChangeWarning -bool false

# Disable warning when emptying Trash
$ defaults write com.apple.finder WarnOnEmptyTrash -bool false

# Don't create .DS_Store on network volumes
$ defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true

# Don't create .DS_Store on USB volumes
$ defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true

# Show icons on desktop
$ defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool true
$ defaults write com.apple.finder ShowHardDrivesOnDesktop -bool false
$ defaults write com.apple.finder ShowMountedServersOnDesktop -bool true
$ defaults write com.apple.finder ShowRemovableMediaOnDesktop -bool true

# New Finder window shows home folder
$ defaults write com.apple.finder NewWindowTarget -string "PfHm"
$ defaults write com.apple.finder NewWindowTargetPath -string "file://${HOME}/"
# PfHm = Home, PfLo = Computer, PfDe = Desktop, PfDo = Documents

# Restart Finder to apply changes
$ killall Finder

Dock Customizations

# Auto-hide the Dock
$ defaults write com.apple.dock autohide -bool true

# Remove auto-hide delay
$ defaults write com.apple.dock autohide-delay -float 0

# Speed up auto-hide animation
$ defaults write com.apple.dock autohide-time-modifier -float 0.3

# Change Dock icon size
$ defaults write com.apple.dock tilesize -int 48

# Enable magnification
$ defaults write com.apple.dock magnification -bool true
$ defaults write com.apple.dock largesize -int 64

# Position: left, bottom, right
$ defaults write com.apple.dock orientation -string "left"

# Minimize windows into application icon
$ defaults write com.apple.dock minimize-to-application -bool true

# Minimize effect: genie, scale, suck
$ defaults write com.apple.dock mineffect -string "scale"

# Don't show recent applications
$ defaults write com.apple.dock show-recents -bool false

# Clear persistent apps (empty Dock)
$ defaults write com.apple.dock persistent-apps -array

# Add spacer tiles
$ defaults write com.apple.dock persistent-apps -array-add '{"tile-type"="spacer-tile";}'

# Restart Dock to apply changes
$ killall Dock

Screenshot Customizations

# Change screenshot location
$ defaults write com.apple.screencapture location -string "${HOME}/Screenshots"

# Change screenshot format (png, jpg, gif, pdf, tiff)
$ defaults write com.apple.screencapture type -string "png"

# Disable shadow in screenshots
$ defaults write com.apple.screencapture disable-shadow -bool true

# Include date in screenshot name
$ defaults write com.apple.screencapture include-date -bool true

# Change screenshot name prefix
$ defaults write com.apple.screencapture name -string "screenshot"

# Apply changes
$ killall SystemUIServer

Safari Customizations

# Enable Develop menu
$ defaults write com.apple.Safari IncludeDevelopMenu -bool true
$ defaults write com.apple.Safari WebKitDeveloperExtrasEnabledPreferenceKey -bool true

# Show full URL in address bar
$ defaults write com.apple.Safari ShowFullURLInSmartSearchField -bool true

# Disable auto-fill
$ defaults write com.apple.Safari AutoFillCreditCardData -bool false
$ defaults write com.apple.Safari AutoFillPasswords -bool false

# Enable "Do Not Track"
$ defaults write com.apple.Safari SendDoNotTrackHTTPHeader -bool true

# Don't open "safe" files after downloading
$ defaults write com.apple.Safari AutoOpenSafeDownloads -bool false

# Show favorites bar
$ defaults write com.apple.Safari ShowFavoritesBar -bool true

# Enable keyboard navigation
$ defaults write com.apple.Safari WebKitTabToLinksPreferenceKey -bool true

# Restart Safari to apply
$ killall Safari

System UI Customizations

# Expand save dialog by default
$ defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode -bool true
$ defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode2 -bool true

# Expand print dialog by default
$ defaults write NSGlobalDomain PMPrintingExpandedStateForPrint -bool true
$ defaults write NSGlobalDomain PMPrintingExpandedStateForPrint2 -bool true

# Disable auto-correct
$ defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false

# Disable auto-capitalization
$ defaults write NSGlobalDomain NSAutomaticCapitalizationEnabled -bool false

# Disable smart quotes
$ defaults write NSGlobalDomain NSAutomaticQuoteSubstitutionEnabled -bool false

# Disable smart dashes
$ defaults write NSGlobalDomain NSAutomaticDashSubstitutionEnabled -bool false

# Disable "natural" scrolling
$ defaults write NSGlobalDomain com.apple.swipescrolldirection -bool false

# Set key repeat rate (lower = faster)
$ defaults write NSGlobalDomain KeyRepeat -int 2

# Set delay until repeat (lower = shorter)
$ defaults write NSGlobalDomain InitialKeyRepeat -int 15

# Enable full keyboard access (Tab through all controls)
$ defaults write NSGlobalDomain AppleKeyboardUIMode -int 3

# Use dark mode
$ defaults write NSGlobalDomain AppleInterfaceStyle -string "Dark"

# Use light mode (delete dark mode setting)
$ defaults delete NSGlobalDomain AppleInterfaceStyle

# Set accent color (Graphite = -1, Red = 0, Orange = 1, etc.)
$ defaults write NSGlobalDomain AppleAccentColor -int 4
# Show battery percentage
$ defaults write com.apple.menuextra.battery ShowPercent -string "YES"

# Show Bluetooth in menu bar
$ defaults write com.apple.controlcenter "NSStatusItem Visible Bluetooth" -bool true

# Show Sound in menu bar
$ defaults write com.apple.controlcenter "NSStatusItem Visible Sound" -bool true

# Show Wi-Fi in menu bar
$ defaults write com.apple.controlcenter "NSStatusItem Visible WiFi" -bool true

# Clock format (customize as needed)
$ defaults write com.apple.menuextra.clock DateFormat -string "EEE d MMM HH:mm:ss"

# Show date in menu bar
$ defaults write com.apple.menuextra.clock ShowDate -int 1

# Flash time separators
$ defaults write com.apple.menuextra.clock FlashDateSeparators -bool false

Text Edit Customizations

# Default to plain text
$ defaults write com.apple.TextEdit RichText -int 0

# Open and save as UTF-8
$ defaults write com.apple.TextEdit PlainTextEncoding -int 4
$ defaults write com.apple.TextEdit PlainTextEncodingForWrite -int 4

Disk Utility

# Show all devices
$ defaults write com.apple.DiskUtility SidebarShowAllDevices -bool true

# Enable advanced features
$ defaults write com.apple.DiskUtility advanced-image-options -bool true

Mission Control and Spaces

# Don't automatically rearrange Spaces
$ defaults write com.apple.dock mru-spaces -bool false

# Group windows by application
$ defaults write com.apple.dock expose-group-apps -bool true

# Hot corners (actions: 0=none, 2=Mission Control, 3=App Windows, 4=Desktop, etc.)
# Bottom left corner: Mission Control
$ defaults write com.apple.dock wvous-bl-corner -int 2
$ defaults write com.apple.dock wvous-bl-modifier -int 0

# Bottom right corner: Desktop
$ defaults write com.apple.dock wvous-br-corner -int 4
$ defaults write com.apple.dock wvous-br-modifier -int 0

$ killall Dock

Security and Privacy

# Require password immediately after sleep
$ defaults write com.apple.screensaver askForPassword -int 1
$ defaults write com.apple.screensaver askForPasswordDelay -int 0

# Enable firewall
$ sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1

# Enable firewall stealth mode
$ sudo defaults write /Library/Preferences/com.apple.alf stealthenabled -int 1

# Disable crash reporter dialog
$ defaults write com.apple.CrashReporter DialogType -string "none"

Reading Preferences from Different Sources

# Read from app container
$ defaults read ~/Library/Containers/com.apple.Safari/Data/Library/Preferences/com.apple.Safari.plist

# Read system-wide preferences
$ sudo defaults read /Library/Preferences/com.apple.loginwindow

# Read from specific plist file
$ defaults read /System/Library/CoreServices/SystemVersion.plist

# Read raw plist (bypassing defaults)
$ plutil -p ~/Library/Preferences/com.apple.finder.plist

# Convert binary plist to XML
$ plutil -convert xml1 -o - ~/Library/Preferences/com.apple.finder.plist

Finding Hidden Preferences

# Watch for preference changes
$ defaults read com.apple.finder > before.txt
# Change a setting in the GUI
$ defaults read com.apple.finder > after.txt
$ diff before.txt after.txt

# Use defaults to export, compare
$ defaults export com.apple.finder - > finder_prefs.xml

# Find keys containing a term
$ defaults read com.apple.finder | grep -i "show"

# Read all domains, search for term
$ defaults domains | tr ',' '\n' | while read domain; do
    defaults read "$domain" 2>/dev/null | grep -l "searchterm" && echo "$domain"
done

Applying Changes: The Restart Requirement

Different changes require different restarts:

DomainRestart Command
Finderkillall Finder
Dockkillall Dock
SystemUIServerkillall SystemUIServer
Safarikillall Safari
Other appskillall AppName
Global changesLog out and back in
System changesRestart

Backup and Restore Preferences

# Export preferences
$ defaults export com.apple.finder ~/finder-backup.plist

# Import preferences
$ defaults import com.apple.finder ~/finder-backup.plist

# Backup all preferences
$ cp -R ~/Library/Preferences ~/preferences-backup

# Export to human-readable format
$ defaults export com.apple.finder - | plutil -convert xml1 -o ~/finder-backup.xml -

Complete Setup Script Example

#!/bin/bash
# macOS customization script

echo "Configuring macOS preferences..."

# Close System Preferences to prevent conflicts
osascript -e 'tell application "System Preferences" to quit'

# ========== Finder ==========
defaults write com.apple.finder AppleShowAllFiles -bool true
defaults write com.apple.finder AppleShowAllExtensions -bool true
defaults write com.apple.finder ShowPathbar -bool true
defaults write com.apple.finder ShowStatusBar -bool true
defaults write com.apple.finder _FXShowPosixPathInTitle -bool true
defaults write com.apple.finder _FXSortFoldersFirst -bool true
defaults write com.apple.finder FXEnableExtensionChangeWarning -bool false
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true

# ========== Dock ==========
defaults write com.apple.dock autohide -bool true
defaults write com.apple.dock autohide-delay -float 0
defaults write com.apple.dock autohide-time-modifier -float 0.3
defaults write com.apple.dock tilesize -int 48
defaults write com.apple.dock show-recents -bool false
defaults write com.apple.dock mineffect -string "scale"

# ========== Screenshots ==========
mkdir -p "${HOME}/Screenshots"
defaults write com.apple.screencapture location -string "${HOME}/Screenshots"
defaults write com.apple.screencapture type -string "png"
defaults write com.apple.screencapture disable-shadow -bool true

# ========== Input ==========
defaults write NSGlobalDomain KeyRepeat -int 2
defaults write NSGlobalDomain InitialKeyRepeat -int 15
defaults write NSGlobalDomain AppleKeyboardUIMode -int 3
defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false
defaults write NSGlobalDomain NSAutomaticQuoteSubstitutionEnabled -bool false
defaults write NSGlobalDomain NSAutomaticDashSubstitutionEnabled -bool false

# ========== Safari ==========
defaults write com.apple.Safari IncludeDevelopMenu -bool true
defaults write com.apple.Safari ShowFullURLInSmartSearchField -bool true
defaults write com.apple.Safari AutoOpenSafeDownloads -bool false

# ========== Security ==========
defaults write com.apple.screensaver askForPassword -int 1
defaults write com.apple.screensaver askForPasswordDelay -int 0

# ========== Apply Changes ==========
echo "Restarting affected applications..."
for app in "Finder" "Dock" "SystemUIServer" "Safari"; do
    killall "${app}" &> /dev/null
done

echo "Done. Some changes may require logout/restart."

Summary

The defaults command gives you complete control over macOS preferences:

TaskCommand
Read all preferencesdefaults read domain
Read specific keydefaults read domain key
Write valuedefaults write domain key -type value
Delete keydefaults delete domain key
Export preferencesdefaults export domain file.plist
Import preferencesdefaults import domain file.plist
List domainsdefaults domains
Find preferencesdefaults find searchterm

Key domains:

DomainPurpose
com.apple.finderFinder preferences
com.apple.dockDock preferences
com.apple.SafariSafari preferences
NSGlobalDomainGlobal/system-wide preferences
com.apple.screencaptureScreenshot preferences
com.apple.desktopservicesDesktop services (.DS_Store, etc.)

Remember to restart the appropriate application or service after making changes. With defaults, you can automate your entire macOS setup and maintain consistent configurations across machines.

Startup and Boot Process

Understanding the macOS boot process helps you troubleshoot startup issues, optimize boot time, and properly configure services. The boot sequence differs significantly between Intel and Apple Silicon Macs, but both ultimately arrive at launchd managing the running system.

Boot Process Overview

Intel Macs

┌─────────────────────────────────────────────────────┐
│ 1. Power On                                         │
│    ↓                                                │
│ 2. EFI/UEFI Firmware                               │
│    • POST (Power-On Self Test)                     │
│    • Initialize hardware                           │
│    • Find boot volume                              │
│    ↓                                                │
│ 3. boot.efi (macOS Boot Loader)                    │
│    • Load kernel and kernel extensions             │
│    ↓                                                │
│ 4. XNU Kernel                                      │
│    • Initialize kernel subsystems                  │
│    • Mount root filesystem                         │
│    • Start launchd (PID 1)                        │
│    ↓                                                │
│ 5. launchd (System)                                │
│    • Load LaunchDaemons                            │
│    • Start system services                         │
│    ↓                                                │
│ 6. loginwindow                                     │
│    • Display login screen                          │
│    ↓                                                │
│ 7. User Session launchd                            │
│    • Load LaunchAgents                             │
│    • Start Dock, Finder, user apps                │
└─────────────────────────────────────────────────────┘

Apple Silicon Macs

┌─────────────────────────────────────────────────────┐
│ 1. Power On                                         │
│    ↓                                                │
│ 2. Boot ROM / iBoot (Secure Enclave)               │
│    • Hardware initialization                       │
│    • Secure boot chain verification               │
│    • Load LLB (Low-Level Bootloader)              │
│    ↓                                                │
│ 3. iBoot Stage 2                                   │
│    • Verify and load kernel collection            │
│    • Authenticate boot assets                      │
│    ↓                                                │
│ 4. XNU Kernel                                      │
│    • Initialize kernel subsystems                  │
│    • Mount signed system volume                   │
│    • Start launchd (PID 1)                        │
│    ↓                                                │
│ 5-7. Same as Intel...                              │
└─────────────────────────────────────────────────────┘

Firmware Stage

Intel: EFI/UEFI

Intel Macs use UEFI firmware:

# View firmware version
$ system_profiler SPHardwareDataType | grep "System Firmware"
      System Firmware Version: 1916.80.2.0.0

# Access EFI variables (limited without SIP disabled)
$ nvram -p
SystemAudioVolume    %80
boot-args
fmm-computer-name    My-Mac

# Set boot arguments (requires SIP disabled)
$ sudo nvram boot-args="-v"  # Verbose boot

Apple Silicon: iBoot

Apple Silicon uses the iBoot chain:

# View boot loader version
$ system_profiler SPHardwareDataType | grep "OS Loader"
      OS Loader Version: 10151.61.4

# Boot ROM version (in Secure Enclave)
$ system_profiler SPHardwareDataType | grep "Boot ROM"
      Boot ROM Version: 10151.61.4

Startup Modes

Intel Macs

Hold keys during startup (after power on, before Apple logo):

KeysMode
Command + RRecovery Mode
OptionStartup Manager (choose boot disk)
ShiftSafe Mode
Command + VVerbose Mode
Command + SSingle User Mode (older macOS)
DApple Diagnostics
NNetBoot
TTarget Disk Mode
Option + Command + RInternet Recovery
Command + Option + P + RReset NVRAM

Apple Silicon Macs

Different process:

ActionHow
Recovery ModeHold Power until “Loading startup options”
Startup ManagerHold Power, then select disk
Safe ModeHold Shift during “Loading startup options”
DiagnosticsHold Power, then Command + D
DFU ModeSpecial button sequence (for restore)
Share DiskIn Recovery, Utilities > Share Disk

Verbose Mode

See what happens during boot:

# Enable verbose boot (Intel)
$ sudo nvram boot-args="-v"

# Apple Silicon: Hold Command+V after selecting boot disk

# Disable verbose boot
$ sudo nvram boot-args=""

Verbose boot shows:

Darwin Kernel Version 23.2.0: ...
AMFI: allowing...
...
IOKit asserts: ...
...
[PID 1] bootstrap: ...

The Kernel: XNU

XNU (X is Not Unix) is a hybrid kernel:

# View kernel version
$ uname -a
Darwin hostname 23.2.0 Darwin Kernel Version 23.2.0: Wed Nov 15 21:53:18 PST 2023; root:xnu-10002.61.3~2/RELEASE_ARM64_T6000 arm64

# Kernel location
$ ls -la /System/Library/Kernels/
-rwxr-xr-x  1 root  wheel  kernel
-rwxr-xr-x  1 root  wheel  kernel.release.t8112
...

# View kernel extensions (kexts)
$ kextstat | head -10
Index Refs Address            Size       Wired      Name (Version)
    1  148 0                  0          0          com.apple.kpi.bsd (23.2.0)
    2   18 0                  0          0          com.apple.kpi.dsep (23.2.0)
    3  181 0                  0          0          com.apple.kpi.iokit (23.2.0)
...

# Kernel cache (pre-linked kernel + kexts)
$ ls -la /System/Library/KernelCollections/

launchd: The Heart of macOS

launchd is always PID 1:

$ ps aux | head -2
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root     1   0.1  0.1 410148544  18624   ??  Ss   10:00AM   0:05.23 /sbin/launchd

launchd manages all services through these directories:

# System daemons (boot-time, root)
/System/Library/LaunchDaemons/   # Apple's
/Library/LaunchDaemons/          # Third-party

# User agents (login-time, user)
/System/Library/LaunchAgents/    # Apple's
/Library/LaunchAgents/           # Third-party, all users
~/Library/LaunchAgents/          # Current user only

Boot-Time Services

# List loaded daemons
$ sudo launchctl list | head -20
PID	Status	Label
-	0	com.apple.airportd
1234	0	com.apple.mds
-	0	com.apple.metadata.mds.index
...

# See what loads at boot
$ ls /Library/LaunchDaemons/
com.docker.vmnetd.plist
com.github.facebook.watchman.plist
homebrew.mxcl.postgresql@14.plist
...

loginwindow and User Sessions

After system services start, loginwindow appears:

# loginwindow process
$ ps aux | grep loginwindow
root 123 ... /System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow console

# Login hook (deprecated but still works)
$ sudo defaults read com.apple.loginwindow LoginHook
# Not set by default

After login, per-user launchd starts:

# User's launchd session
$ launchctl print user/$(id -u)
com.apple.xpc.launchd.domain.user.501 = {
    type = user
    handle = 501
    ...
    services = {
        com.apple.Dock.agent = { ... }
        com.apple.Finder = { ... }
        ...
    }
}

Login Items

Login items run when a user logs in:

Modern Login Items (macOS 13+)

# View login items
$ sfltool dumpbtm

Service Management Framework

# View login items for current user
$ osascript -e 'tell application "System Events" to get the name of every login item'

Adding Login Items

# Add login item via AppleScript
$ osascript -e 'tell application "System Events" to make login item at end with properties {name:"MyApp", path:"/Applications/MyApp.app", hidden:false}'

# Remove login item
$ osascript -e 'tell application "System Events" to delete login item "MyApp"'

Launch Agents (Preferred)

Better than login items for services:

# Create user launch agent
$ cat > ~/Library/LaunchAgents/com.example.myagent.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.myagent</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/mycommand</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>
EOF

# Load the agent
$ launchctl load ~/Library/LaunchAgents/com.example.myagent.plist

Startup Items (Deprecated)

Old-style startup items still exist but are deprecated:

# Legacy locations (avoid using)
/Library/StartupItems/
/System/Library/StartupItems/

Controlling Boot Behavior

systemsetup

# View startup disk
$ sudo systemsetup -getstartupdisk
Startup Disk: Macintosh HD

# Set startup disk
$ sudo systemsetup -setstartupdisk "Macintosh HD"

# Enable/disable startup from network
$ sudo systemsetup -setnetworkstartup on

# Set startup after power failure
$ sudo systemsetup -setrestartpowerfailure on

# Set startup after freeze
$ sudo systemsetup -setrestartfreeze on

# Wait for network at boot
$ sudo systemsetup -setwaitforstartupafterpowerfailure 30

bless

# View blessed (bootable) systems
$ sudo bless --info /
finderinfo[0]:     64 => Blessed System Folder is /System/Library/CoreServices
finderinfo[1]:      0 => No Blessed System File
finderinfo[2]:      0 => Open-folder linked list empty
finderinfo[3]:      0 => No OS 9 + X blessed 9 folder
finderinfo[4]:      0 => Unused field unset
finderinfo[5]:     64 => OS X blessed folder is /System/Library/CoreServices
64-bit VSDB volume id:  0xXXXXXXXXXXXXXXXX

# Set boot volume
$ sudo bless --mount /Volumes/OtherDisk --setBoot

nvram

# View all NVRAM variables
$ nvram -xp

# Common variables
$ nvram boot-args        # Kernel boot arguments
$ nvram SystemAudioVolume  # Startup sound

# Set boot arguments (Intel, SIP must be disabled)
$ sudo nvram boot-args="-v"           # Verbose
$ sudo nvram boot-args="debug=0x100"  # Wait for debugger
$ sudo nvram boot-args="-x"           # Safe mode

# Reset NVRAM from command line
$ sudo nvram -c

Boot Time Analysis

# Time since boot
$ uptime
10:30  up 5 days, 12:45, 3 users, load averages: 1.75 2.20 2.10

# Exact boot time
$ sysctl kern.boottime
kern.boottime: { sec = 1705234567, usec = 0 } Mon Jan 15 10:00:00 2024

# Last boot time
$ last reboot
reboot    ~                         Mon Jan 15 10:00

# Boot performance (what took time)
$ log show --predicate 'process == "launchd"' --start "$(sysctl -n kern.boottime | awk '{print $4}' | tr -d ',')" --last 2m | head -50

Troubleshooting Boot Issues

Safe Mode

Safe Mode disables third-party extensions and performs basic checks:

# Intel: Hold Shift during boot
# Apple Silicon: Hold Shift after selecting disk

# Check if in Safe Mode
$ sysctl -n kern.safeboot
1    # Safe mode
0    # Normal mode

# Or via System Information
$ system_profiler SPSoftwareDataType | grep "Boot Mode"
      Boot Mode: Safe

Verbose Boot

See what’s happening:

# Enable for Intel
$ sudo nvram boot-args="-v"

# Reboot
$ sudo reboot

# Disable after troubleshooting
$ sudo nvram boot-args=""

Single User Mode (Intel, Older macOS)

# Boot with Command+S
# At prompt:
/sbin/fsck -fy          # Check filesystem
/sbin/mount -uw /       # Mount filesystem read-write
# Make changes...
exit                    # Continue boot

Reset NVRAM/PRAM

# Intel: Reboot, hold Command+Option+P+R for 20 seconds

# Or from command line
$ sudo nvram -c
$ sudo reboot

Rebuild Kext Cache

# Rebuild kernel cache
$ sudo kextcache --clear-staging
$ sudo kextcache -u /

# On Apple Silicon with SSV
$ sudo kmutil install --update-all

Automatic Login

# Check if auto-login is enabled
$ sudo defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser
davidsmith

# Enable auto-login (stored in protected location)
# Better to use System Settings > Users & Groups > Login Options

Power Events

# View wake/sleep events
$ pmset -g log | tail -20

# View scheduled events
$ pmset -g sched

# Schedule wake
$ sudo pmset schedule wake "01/20/2024 08:00:00"

# Schedule sleep
$ sudo pmset schedule sleep "01/20/2024 23:00:00"

# Cancel scheduled event
$ sudo pmset schedule cancel wake

Summary

The macOS boot process:

StageComponentPurpose
1Firmware (EFI/iBoot)Hardware init, secure boot
2Boot loaderLoad kernel
3XNU KernelCore OS, mount root
4launchd (system)Start system services
5loginwindowUser authentication
6launchd (user)Start user services

Key startup modes:

ModePurposeHow
RecoveryRepair, reinstallCmd+R / Hold Power
SafeDisable extensionsShift
VerboseSee boot messagesCmd+V
Target DiskShare diskT (Intel)

Essential commands:

# Boot info
$ uptime
$ sysctl kern.boottime
$ nvram -p

# Service management
$ launchctl list
$ sudo launchctl load /path/to/plist

# Boot configuration
$ sudo systemsetup -getstartupdisk
$ sudo bless --info /

# Troubleshooting
$ log show --predicate 'process == "launchd"' --last 5m
$ kextstat

Understanding the boot process helps you configure services correctly, troubleshoot startup problems, and maintain system reliability.

Recovery Mode and Troubleshooting

Recovery Mode is macOS’s built-in rescue environment. It provides tools to repair disks, reinstall the operating system, restore from backups, and access Terminal for advanced troubleshooting. Every macOS administrator should know how to access and use Recovery Mode effectively.

Accessing Recovery Mode

Intel Macs

  1. Restart your Mac (or turn it on)
  2. Immediately press and hold Command + R
  3. Keep holding until you see the Apple logo or spinning globe
  4. Release when you see the macOS Utilities window

Alternative boot options:

KeysResult
Command + RRecovery from local partition
Option + Command + RInternet Recovery (latest macOS)
Shift + Option + Command + RInternet Recovery (original macOS)

Apple Silicon Macs

  1. Shut down your Mac completely
  2. Press and hold the Power button
  3. Keep holding until “Loading startup options” appears
  4. Click Options, then Continue
  5. Select a user account and enter password if prompted
  6. You’ll see the Recovery utilities

Recovery Mode Interface

Recovery Mode presents macOS Utilities:

┌─────────────────────────────────────────────────────┐
│                  macOS Utilities                    │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌───────────────────────┐  ┌───────────────────┐  │
│  │ Restore from Time     │  │ Reinstall macOS   │  │
│  │ Machine               │  │ Sonoma            │  │
│  └───────────────────────┘  └───────────────────┘  │
│                                                     │
│  ┌───────────────────────┐  ┌───────────────────┐  │
│  │ Safari                │  │ Disk Utility      │  │
│  │                       │  │                   │  │
│  └───────────────────────┘  └───────────────────┘  │
│                                                     │
│  Menu bar: Apple | Recovery | File | Edit | Window │
│                    Utilities menu → Terminal        │
└─────────────────────────────────────────────────────┘

Access Terminal from: Utilities > Terminal

Using Terminal in Recovery Mode

Recovery Terminal is a full bash shell with root privileges:

# You're automatically root
$ whoami
root

# Filesystem is mounted read-only by default
$ mount
/dev/disk1s5 on / (apfs, local, read-only, journaled)
...

# The main volume is usually mounted at /Volumes
$ ls /Volumes/
Macintosh HD
Macintosh HD - Data

Mount the Main Filesystem Read-Write

# Find the main volume
$ diskutil list
/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *500.1 GB   disk0
   1:             Apple_APFS_ISC                         524.3 MB   disk0s1
   2:                 Apple_APFS Container disk3         494.4 GB   disk0s2
   3:        Apple_APFS_Recovery                         5.4 GB     disk0s3

/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +494.4 GB   disk3
   1:                APFS Volume Macintosh HD            15.2 GB    disk3s1
   2:              APFS Snapshot com.apple.os.update-... 15.2 GB    disk3s1s1
   3:                APFS Volume Macintosh HD - Data     154.3 GB   disk3s2
   ...

# Mount read-write (if not already)
$ mount -uw /Volumes/Macintosh\ HD

Common Recovery Tasks

Reset Administrator Password

If you’ve forgotten your admin password:

# In Recovery Terminal:

# Method 1: Using resetpassword tool (GUI)
$ resetpassword
# This opens Reset Password utility

# Method 2: Command line (older macOS)
# Find the user
$ ls /Volumes/Macintosh\ HD/Users/
admin
david
Shared

# Reset password using dscl
$ dscl -f "/Volumes/Macintosh HD/var/db/dslocal/nodes/Default" localonly -passwd /Local/Default/Users/david newpassword

# Or using Directory Services
$ passwd -u david
Changing password for david.
New password:
Retype new password:

Note: On Apple Silicon with Secure Enclave, you may need to authenticate with another admin account or use Apple ID recovery.

Enable Root User

# In Recovery Terminal:
$ dscl -f "/Volumes/Macintosh HD/var/db/dslocal/nodes/Default" localonly -passwd /Local/Default/Users/root newpassword

Create New Admin User

# In Recovery Terminal:

# Change to the mounted volume
$ cd "/Volumes/Macintosh HD"

# Create user with dscl
$ dscl -f var/db/dslocal/nodes/Default localonly -create /Local/Default/Users/rescue
$ dscl -f var/db/dslocal/nodes/Default localonly -create /Local/Default/Users/rescue UserShell /bin/zsh
$ dscl -f var/db/dslocal/nodes/Default localonly -create /Local/Default/Users/rescue RealName "Rescue Admin"
$ dscl -f var/db/dslocal/nodes/Default localonly -create /Local/Default/Users/rescue UniqueID 550
$ dscl -f var/db/dslocal/nodes/Default localonly -create /Local/Default/Users/rescue PrimaryGroupID 20
$ dscl -f var/db/dslocal/nodes/Default localonly -create /Local/Default/Users/rescue NFSHomeDirectory /Users/rescue
$ dscl -f var/db/dslocal/nodes/Default localonly -passwd /Local/Default/Users/rescue rescuepassword

# Add to admin group
$ dscl -f var/db/dslocal/nodes/Default localonly -append /Local/Default/Groups/admin GroupMembership rescue

# Create home directory
$ mkdir -p Users/rescue
$ chown 550:20 Users/rescue

Disable System Integrity Protection (SIP)

# In Recovery Terminal:

# Check current status
$ csrutil status
System Integrity Protection status: enabled.

# Disable SIP
$ csrutil disable
Successfully disabled System Integrity Protection. Please restart the machine for the changes to take effect.

# Selectively disable
$ csrutil enable --without kext
$ csrutil enable --without fs

# Re-enable SIP
$ csrutil enable

Repair Disk

Using Disk Utility GUI:

  1. Open Disk Utility
  2. Select the disk/volume
  3. Click “First Aid”
  4. Click “Run”

Using Terminal:

# List disks
$ diskutil list

# Repair volume
$ diskutil repairVolume /dev/disk3s1

# Repair disk (entire disk)
$ diskutil repairDisk /dev/disk0

# For HFS+ filesystems
$ /sbin/fsck_hfs -fy /dev/disk3s1

# For APFS
$ /sbin/fsck_apfs -y /dev/disk3s1

# Repair permissions (older macOS)
$ diskutil repairPermissions /

Erase and Reinstall

From Disk Utility:

  1. Select the internal drive (container)
  2. Click “Erase”
  3. Name it, choose APFS format
  4. Quit Disk Utility
  5. Choose “Reinstall macOS”

From Terminal:

# WARNING: This erases everything!

# List disks
$ diskutil list

# Erase and format as APFS
$ diskutil eraseDisk APFS "Macintosh HD" disk0

# Or erase just the container
$ diskutil apfs deleteContainer disk3
$ diskutil apfs createContainer disk0s2
$ diskutil apfs addVolume disk3 APFS "Macintosh HD"

Restore from Time Machine

GUI method:

  1. Choose “Restore from Time Machine”
  2. Select Time Machine disk
  3. Choose backup to restore
  4. Select destination disk
  5. Click “Restore”

Terminal method:

# List Time Machine backups
$ tmutil listbackups

# Restore specific files
$ tmutil restore /Volumes/Time\ Machine/Backups.backupdb/MacName/Latest/Macintosh\ HD/Users/david/file.txt /Volumes/Macintosh\ HD/Users/david/

# Full restore (use GUI for this)

Copy Files from Broken System

# Mount an external drive
$ diskutil list    # Find external drive
$ diskutil mount /dev/disk4s1

# Copy files
$ cp -R "/Volumes/Macintosh HD/Users/david/Documents" /Volumes/External/

# Or use ditto (preserves metadata)
$ ditto "/Volumes/Macintosh HD/Users/david/Documents" /Volumes/External/Documents

Network in Recovery Mode

# Check network
$ ifconfig en0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255
    ...

# Wi-Fi (if not connected via menu bar)
$ networksetup -setairportnetwork en0 "NetworkName" "password"

# Test connectivity
$ ping -c 3 apple.com

# Download something
$ curl -O https://example.com/file

Reinstall macOS from Terminal

# Start the installer
$ /Volumes/Macintosh\ HD/Applications/Install\ macOS\ Sonoma.app/Contents/Resources/startosinstall --agreetolicense --volume /Volumes/Macintosh\ HD

# For Internet Recovery, the installer downloads automatically

Apple Silicon Specific

Startup Security Utility

In Recovery Mode, you can access Startup Security Utility:

  1. Utilities > Startup Security Utility
  2. Options:
    • Full Security: Only current, signed macOS
    • Reduced Security: Allows older signed macOS
    • Permissive Security: Allows unsigned kexts
# From Terminal, check security policy
$ bputil -d

Change Security Settings

# Allow kernel extensions
$ spctl kext-consent add TEAMID

# View kext consent list
$ spctl kext-consent list

Share Disk (Target Disk Mode Alternative)

  1. In Recovery Mode
  2. Utilities > Share Disk
  3. Select disk to share
  4. Connect to another Mac and access via Finder

Safe Mode

Boot into Safe Mode for basic troubleshooting:

Intel Macs

Hold Shift during startup

Apple Silicon Macs

  1. Hold Power button
  2. Select disk, hold Shift
  3. Click “Continue in Safe Mode”

Safe Mode:

  • Disables login items
  • Loads only required kernel extensions
  • Clears font caches
  • Clears kernel cache
  • Runs basic disk check
# Check if in Safe Mode
$ sysctl kern.safeboot
kern.safeboot: 1

Verbose Mode

See boot messages for troubleshooting:

Intel Macs

Hold Command + V during startup

Apple Silicon Macs

  1. Hold Power button
  2. Select disk, press Command + V

Or set permanently:

# Enable verbose boot (Intel)
$ sudo nvram boot-args="-v"

Single User Mode (Deprecated)

Single User Mode is no longer available on Apple Silicon and modern Intel Macs with T2 chip. Use Recovery Mode Terminal instead.

On older Intel Macs:

  1. Hold Command + S during startup
  2. Arrives at root shell
  3. Run repairs:
/sbin/fsck -fy
/sbin/mount -uw /
# Make changes
exit

Troubleshooting Boot Issues

Mac Won’t Start

  1. Check power: Hold power 10 seconds, release, press again
  2. Reset SMC (Intel):
    • Shut down
    • Hold Shift + Control + Option + Power for 10 seconds
    • Release all keys, press power
  3. Reset NVRAM (Intel):
    • Restart, hold Command + Option + P + R for 20 seconds
  4. Safe Mode: Hold Shift (eliminates software issues)
  5. Recovery Mode: Command + R (repair or reinstall)
  6. Apple Diagnostics: Hold D (hardware test)

Mac Stuck at Login

  1. Boot to Safe Mode (Shift)
  2. Remove login items:
# In Recovery Terminal
$ rm /Volumes/Macintosh\ HD/Users/username/Library/Preferences/com.apple.loginitems.plist
  1. Check LaunchAgents:
$ ls "/Volumes/Macintosh HD/Users/username/Library/LaunchAgents/"
# Move problematic plists
$ mv "/Volumes/Macintosh HD/Users/username/Library/LaunchAgents/suspect.plist" /tmp/

Application Causing Problems

# In Recovery Terminal

# Remove app's preferences
$ rm "/Volumes/Macintosh HD/Users/username/Library/Preferences/com.problem.app.plist"

# Remove app's caches
$ rm -rf "/Volumes/Macintosh HD/Users/username/Library/Caches/com.problem.app"

# Remove app's application support
$ rm -rf "/Volumes/Macintosh HD/Users/username/Library/Application Support/Problem App"

Kernel Panic on Boot

# In Recovery Terminal

# Check for bad kexts in third-party location
$ ls "/Volumes/Macintosh HD/Library/Extensions/"

# Move suspect kext
$ mv "/Volumes/Macintosh HD/Library/Extensions/SuspectDriver.kext" /tmp/

# Rebuild kext cache
$ kmutil install --update-all --volume "/Volumes/Macintosh HD"

Internet Recovery

If Recovery partition is damaged:

Intel Macs

  • Option + Command + R: Latest compatible macOS
  • Shift + Option + Command + R: Original macOS that came with Mac

Apple Silicon Macs

Internet Recovery is automatic if local recovery fails.

Requirements:

  • Network connection (Ethernet or Wi-Fi)
  • Connection to Apple’s servers
  • May take 30-60+ minutes depending on connection

Recovery Logs

# View recovery session logs
$ log show --predicate 'subsystem == "com.apple.install"' --last 1h

# Check install logs
$ cat /var/log/install.log

Summary

Recovery Mode is your primary tool for macOS troubleshooting:

TaskMethod
Access RecoveryCmd+R (Intel) / Hold Power (AS)
Reset passwordRecovery > Utilities > Terminal
Disable SIPRecovery > Terminal > csrutil disable
Repair diskDisk Utility > First Aid
Reinstall macOSRecovery > Reinstall macOS
Restore backupRecovery > Restore from Time Machine
Safe ModeHold Shift
Verbose ModeCmd+V

Key commands in Recovery Terminal:

# Disk operations
diskutil list
diskutil repairVolume /dev/diskXsY
diskutil eraseDisk APFS "Name" diskX

# Password reset
resetpassword  # GUI tool
dscl -f "/path/to/dslocal" localonly -passwd /Local/Default/Users/name password

# SIP management
csrutil status
csrutil disable
csrutil enable

# File recovery
cp -R /Volumes/Source/path /Volumes/Dest/path
ditto /source /dest

Remember: Recovery Mode provides root access to your system. Use it carefully, especially when modifying system files or resetting passwords.

Networking on macOS

macOS provides a sophisticated networking stack that blends traditional BSD networking tools with Apple’s modern network configuration frameworks. If you’re coming from Linux, you’ll find familiar concepts but different tools: there’s no ip command, no NetworkManager, and no systemd-networkd. Instead, macOS uses a combination of BSD utilities and Apple-specific tools like networksetup, scutil, and the configd daemon.

The macOS Network Architecture

At its core, macOS networking is built on:

┌─────────────────────────────────────────────────────────────┐
│                    Applications                              │
├─────────────────────────────────────────────────────────────┤
│                    Network Framework                         │
│              (URLSession, Network.framework)                 │
├─────────────────────────────────────────────────────────────┤
│  SystemConfiguration    │    Bonjour/mDNS    │    VPN       │
│     Framework           │   (mDNSResponder)  │   Framework  │
├─────────────────────────────────────────────────────────────┤
│                    configd daemon                            │
│              (System Configuration Agent)                    │
├─────────────────────────────────────────────────────────────┤
│                    BSD Network Stack                         │
│              (Sockets, TCP/IP, Routing)                     │
├─────────────────────────────────────────────────────────────┤
│                    XNU Kernel                                │
│        (Network interfaces, drivers, packet filtering)      │
└─────────────────────────────────────────────────────────────┘

Key Components

configd: The system configuration daemon that manages network state, detects changes, and notifies applications.

mDNSResponder: Handles Bonjour/mDNS, multicast DNS, and DNS service discovery. Also serves as the local DNS resolver.

SystemConfiguration Framework: Provides APIs for network configuration and state monitoring.

Command-Line Tools Overview

macOS provides several categories of networking tools:

Configuration Tools

ToolPurpose
networksetupHigh-level network service configuration
ipconfigDHCP and IP configuration
ifconfigInterface configuration (BSD)
scutilSystem configuration utility

Diagnostic Tools

ToolPurpose
pingTest host reachability
tracerouteTrace packet route
mtrCombined ping/traceroute (via Homebrew)
netstatNetwork statistics
nettopReal-time network activity
tcpdumpPacket capture
networkQualityNetwork performance test (macOS 12+)

DNS and Discovery

ToolPurpose
digDNS lookup
hostSimple DNS lookup
nslookupDNS lookup (legacy)
dscacheutilDirectory services cache
dns-sdDNS service discovery

Wi-Fi Tools

ToolPurpose
airportWi-Fi diagnostics (hidden utility)
networksetupWi-Fi configuration
wdutilWireless diagnostics

Quick Network Status Check

Get a quick overview of your network configuration:

# Show all active interfaces with IPs
$ ifconfig | grep -E "^[a-z]|inet "
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
	inet 127.0.0.1 netmask 0xff000000
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255

# Show current default gateway
$ route get default | grep gateway
    gateway: 192.168.1.1

# Show DNS servers
$ scutil --dns | grep nameserver | head -5
  nameserver[0] : 192.168.1.1
  nameserver[1] : 8.8.8.8

# Show active network service
$ networksetup -listallnetworkservices | head -3
An asterisk (*) denotes that a network service is disabled.
Wi-Fi
Ethernet

Network Locations

macOS supports “Locations”—saved network configurations you can switch between:

# List locations
$ networksetup -listlocations
Automatic
Home
Office

# Get current location
$ networksetup -getcurrentlocation
Automatic

# Switch location
$ networksetup -switchtolocation "Office"

Locations are useful for laptops that move between different network environments with different proxy settings, DNS servers, or IP configurations.

What You’ll Learn in This Part

Network Configuration from CLI covers the essential tools for configuring network interfaces, IP addresses, DNS, and routing using networksetup, ipconfig, ifconfig, and scutil.

Understanding Network Services explains how macOS organizes network interfaces into services, how to manage network locations, and configure advanced features like VLANs.

Bonjour and mDNS introduces Apple’s zero-configuration networking technology, including how to browse and advertise services using the dns-sd command.

VPN Configuration shows how to configure and manage VPN connections from the command line, including IKEv2, L2TP/IPSec, and third-party VPN solutions.

Sharing Services via Command Line explains how to enable and configure macOS sharing services like SSH, Screen Sharing, and File Sharing from Terminal.

Network Diagnostics and Troubleshooting provides a comprehensive guide to troubleshooting network issues using ping, traceroute, mtr, nettop, tcpdump, and other diagnostic tools.

Wi-Fi Management from Terminal covers the hidden airport utility and other tools for scanning networks, connecting, and diagnosing Wi-Fi issues.

Quick Troubleshooting Guide

Common network issues and quick fixes:

Can’t Connect to Network

# Check if interface is up
$ ifconfig en0 | grep status
	status: active

# Renew DHCP lease
$ sudo ipconfig set en0 DHCP

# Restart network service
$ sudo ifconfig en0 down && sudo ifconfig en0 up

DNS Not Resolving

# Flush DNS cache
$ sudo dscacheutil -flushcache
$ sudo killall -HUP mDNSResponder

# Check DNS servers
$ scutil --dns | grep nameserver

# Test with specific DNS server
$ dig @8.8.8.8 example.com

Slow Network

# Test network quality (macOS 12+)
$ networkQuality -s

# Check for packet loss
$ ping -c 100 gateway_ip | grep "packet loss"

# Monitor real-time network activity
$ nettop -m tcp

Check What’s Using the Network

# See network connections by process
$ lsof -i -P | grep ESTABLISHED

# Real-time network usage by process
$ nettop -P

# See what's listening
$ lsof -i -P | grep LISTEN

The following chapters dive deep into each of these areas, providing the knowledge you need to configure, secure, and troubleshoot networking on macOS.

Network Configuration from CLI

macOS provides several command-line tools for network configuration, each operating at a different level of abstraction. Understanding when to use each tool is key to effective network management.

The Configuration Tools Hierarchy

High-level    networksetup    User-friendly, persistent settings
                  │
                  ▼
Mid-level       ipconfig       DHCP operations, BootP
                  │
                  ▼
Low-level      ifconfig        Direct interface manipulation (BSD)
                  │
                  ▼
System         scutil          System configuration framework access

networksetup: The Primary Configuration Tool

networksetup is the command-line equivalent of System Preferences > Network. Changes made with networksetup are persistent and integrated with macOS’s network management.

Listing Network Services

# List all network services
$ networksetup -listallnetworkservices
An asterisk (*) denotes that a network service is disabled.
Wi-Fi
USB 10/100/1000 LAN
Thunderbolt Bridge
iPhone USB
*Bluetooth PAN

# List hardware ports with device names
$ networksetup -listallhardwareports

Hardware Port: Wi-Fi
Device: en0
Ethernet Address: aa:bb:cc:dd:ee:ff

Hardware Port: Thunderbolt 1
Device: en1
Ethernet Address: bb:cc:dd:ee:ff:00

Hardware Port: Thunderbolt Bridge
Device: bridge0
Ethernet Address: cc:dd:ee:ff:00:11

Getting Network Information

# Get detailed info for a service
$ networksetup -getinfo "Wi-Fi"
DHCP Configuration
IP address: 192.168.1.100
Subnet mask: 255.255.255.0
Router: 192.168.1.1
Client ID:
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Wi-Fi ID: aa:bb:cc:dd:ee:ff

# Get just the IP address
$ networksetup -getinfo "Wi-Fi" | grep "IP address" | head -1
IP address: 192.168.1.100

# Get computer name
$ networksetup -getcomputername
My-MacBook-Pro

IP Address Configuration

# Set static IP address
$ sudo networksetup -setmanual "Wi-Fi" 192.168.1.50 255.255.255.0 192.168.1.1
#                                service    IP          netmask       gateway

# Verify the change
$ networksetup -getinfo "Wi-Fi"
Manual Configuration
IP address: 192.168.1.50
Subnet mask: 255.255.255.0
Router: 192.168.1.1

# Switch back to DHCP
$ sudo networksetup -setdhcp "Wi-Fi"

# Set DHCP with manual IP (DHCP for DNS, manual IP)
$ sudo networksetup -setdhcp "Wi-Fi" 192.168.1.50

DNS Configuration

# View current DNS servers
$ networksetup -getdnsservers "Wi-Fi"
8.8.8.8
8.8.4.4

# Set DNS servers
$ sudo networksetup -setdnsservers "Wi-Fi" 8.8.8.8 8.8.4.4 1.1.1.1

# Clear DNS (use DHCP-provided servers)
$ sudo networksetup -setdnsservers "Wi-Fi" empty

# Verify DNS settings
$ scutil --dns | grep "nameserver\[" | head -5
  nameserver[0] : 8.8.8.8
  nameserver[1] : 8.8.4.4
  nameserver[2] : 1.1.1.1

Search Domains

# View search domains
$ networksetup -getsearchdomains "Wi-Fi"
example.com
internal.example.com

# Set search domains
$ sudo networksetup -setsearchdomains "Wi-Fi" example.com internal.example.com

# Clear search domains
$ sudo networksetup -setsearchdomains "Wi-Fi" empty

MTU Configuration

# Get current MTU
$ networksetup -getMTU "Wi-Fi"
Active MTU: 1500 (Current Setting: 1500)

# Set MTU
$ sudo networksetup -setMTU "Wi-Fi" 1400

# Reset to default
$ sudo networksetup -setMTU "Wi-Fi" 1500

Proxy Configuration

# Get web proxy settings
$ networksetup -getwebproxy "Wi-Fi"
Enabled: No
Server:
Port: 0
Authenticated Proxy Enabled: 0

# Set web proxy (HTTP)
$ sudo networksetup -setwebproxy "Wi-Fi" proxy.example.com 8080

# Set with authentication
$ sudo networksetup -setwebproxy "Wi-Fi" proxy.example.com 8080 on username password

# Enable/disable web proxy
$ sudo networksetup -setwebproxystate "Wi-Fi" on
$ sudo networksetup -setwebproxystate "Wi-Fi" off

# Set secure web proxy (HTTPS)
$ sudo networksetup -setsecurewebproxy "Wi-Fi" proxy.example.com 8080

# Set SOCKS proxy
$ sudo networksetup -setsocksfirewallproxy "Wi-Fi" proxy.example.com 1080

# Set proxy auto-config (PAC) URL
$ sudo networksetup -setautoproxyurl "Wi-Fi" "http://proxy.example.com/proxy.pac"

# Set bypass domains (proxy exceptions)
$ sudo networksetup -setproxybypassdomains "Wi-Fi" "*.local" "169.254/16" "localhost"

# Get all proxy settings
$ networksetup -getproxybypassdomains "Wi-Fi"
*.local
169.254/16
localhost

Service Order

# View network service order (priority)
$ networksetup -listnetworkserviceorder
An asterisk (*) denotes that a network service is disabled.
(1) Wi-Fi
(Hardware Port: Wi-Fi, Device: en0)

(2) USB 10/100/1000 LAN
(Hardware Port: USB 10/100/1000 LAN, Device: en7)

(3) Thunderbolt Bridge
(Hardware Port: Thunderbolt Bridge, Device: bridge0)

# Set service order (first service has highest priority)
$ sudo networksetup -ordernetworkservices "Ethernet" "Wi-Fi" "Thunderbolt Bridge"

ipconfig: DHCP and BootP Operations

ipconfig handles DHCP client operations and provides detailed interface information.

Getting Interface Information

# Get all information for an interface
$ ipconfig getifaddr en0
192.168.1.100

# Get subnet mask
$ ipconfig getoption en0 subnet_mask
255.255.255.0

# Get router (gateway)
$ ipconfig getoption en0 router
192.168.1.1

# Get DHCP server address
$ ipconfig getoption en0 server_identifier
192.168.1.1

# Get DNS servers from DHCP
$ ipconfig getoption en0 domain_name_server
192.168.1.1

# Get lease duration
$ ipconfig getoption en0 lease_time
86400

# Get domain name from DHCP
$ ipconfig getoption en0 domain_name
home

DHCP Operations

# Renew DHCP lease
$ sudo ipconfig set en0 DHCP

# Request specific IP via DHCP (DHCP INFORM)
$ sudo ipconfig set en0 INFORM 192.168.1.50

# Release DHCP lease (sets to none/unconfigured)
$ sudo ipconfig set en0 NONE

# Set manual IP (temporary, until reboot)
$ sudo ipconfig set en0 MANUAL 192.168.1.50 255.255.255.0

Viewing DHCP Packet Information

# Show DHCP packet details
$ ipconfig getpacket en0
op = BOOTREPLY
htype = 1
flags = 0
hlen = 6
hops = 0
xid = 0x12345678
secs = 0
ciaddr = 0.0.0.0
yiaddr = 192.168.1.100
siaddr = 0.0.0.0
giaddr = 0.0.0.0
chaddr = aa:bb:cc:dd:ee:ff
sname =
file =
options:
Options count is 10
dhcp_message_type (uint8): ACK 0x5
server_identifier (ip): 192.168.1.1
lease_time (uint32): 0x15180 (86400)
subnet_mask (ip): 255.255.255.0
router (ip_mult): 192.168.1.1
domain_name_server (ip_mult): 192.168.1.1
domain_name (string): home
end (none):

IPv6 Configuration

# Get IPv6 address
$ ipconfig getv6addr en0
fe80::1c1c:abcd:1234:5678%en0

# Set IPv6 to automatic
$ sudo ipconfig set en0 AUTOMATIC-V6

# Set manual IPv6
$ sudo ipconfig set en0 MANUAL-V6 2001:db8::1 64

# Disable IPv6 on interface
$ sudo networksetup -setv6off "Wi-Fi"

# Enable IPv6 automatic
$ sudo networksetup -setv6automatic "Wi-Fi"

ifconfig: BSD Interface Configuration

ifconfig provides low-level interface control. Changes are temporary and don’t survive reboots. Use networksetup for persistent changes.

Viewing Interface Status

# Show all interfaces
$ ifconfig -a

# Show specific interface
$ ifconfig en0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	options=6463<RXCSUM,TXCSUM,TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM>
	ether aa:bb:cc:dd:ee:ff
	inet6 fe80::1c1c:abcd:1234:5678%en0 prefixlen 64 secured scopeid 0x8
	inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255
	nd6 options=201<PERFORMNUD,DAD>
	media: autoselect
	status: active

# Show only UP interfaces
$ ifconfig -u

# Show only DOWN interfaces
$ ifconfig -d

# List interface names only
$ ifconfig -l
lo0 gif0 stf0 en0 en1 en2 bridge0 utun0 utun1

# Get just the IP address (parsing)
$ ifconfig en0 | awk '/inet / {print $2}'
192.168.1.100

Understanding Interface Flags

UP          - Interface is enabled
BROADCAST   - Supports broadcast
SMART       - Interface manages own state
RUNNING     - Interface is operational
SIMPLEX     - Can't hear own transmissions
MULTICAST   - Supports multicast
PROMISC     - Promiscuous mode (captures all packets)

Enabling and Disabling Interfaces

# Bring interface down
$ sudo ifconfig en0 down

# Bring interface up
$ sudo ifconfig en0 up

# Enable promiscuous mode
$ sudo ifconfig en0 promisc

# Disable promiscuous mode
$ sudo ifconfig en0 -promisc

Temporary IP Configuration

# Set IP address (temporary)
$ sudo ifconfig en0 inet 192.168.1.50 netmask 255.255.255.0

# Add an alias IP (second IP on same interface)
$ sudo ifconfig en0 alias 192.168.1.51 netmask 255.255.255.0

# Remove alias
$ sudo ifconfig en0 -alias 192.168.1.51

# Set broadcast address
$ sudo ifconfig en0 broadcast 192.168.1.255

MTU and Media Configuration

# Change MTU
$ sudo ifconfig en0 mtu 1400

# View media options
$ ifconfig en0 | grep media
	media: autoselect

# For Ethernet, set specific media (example)
$ sudo ifconfig en1 media 1000baseT mediaopt full-duplex

scutil: System Configuration Utility

scutil provides access to the System Configuration framework, offering both interactive and command-line modes.

Hostname Configuration

macOS has three types of hostnames:

# ComputerName: User-friendly name shown in Finder
$ scutil --get ComputerName
My MacBook Pro

# LocalHostName: Bonjour name (.local)
$ scutil --get LocalHostName
My-MacBook-Pro

# HostName: BSD hostname (may not be set)
$ scutil --get HostName
my-macbook-pro.example.com

# Set hostnames (requires sudo)
$ sudo scutil --set ComputerName "Work MacBook"
$ sudo scutil --set LocalHostName "Work-MacBook"
$ sudo scutil --set HostName "work-macbook.example.com"

DNS Information

# Show complete DNS configuration
$ scutil --dns
DNS configuration

resolver #1
  search domain[0] : home
  nameserver[0] : 192.168.1.1
  nameserver[1] : 8.8.8.8
  if_index : 8 (en0)
  flags    : Request A records
  reach    : 0x00000002 (Reachable)

resolver #2
  domain   : local
  options  : mdns
  timeout  : 5
  flags    : Request A records
  reach    : 0x00000000 (Not Reachable)
  order    : 300000
...

Network Reachability

# Check if host is reachable
$ scutil -r google.com
Reachable

$ scutil -r 192.168.1.1
Reachable,Direct

$ scutil -r 10.0.0.1
Not Reachable

# Reachability flags explained:
# Reachable          - Can reach destination
# Direct             - Directly connected (same subnet)
# Transient Current  - Using temporary connection
# Connection Required - Need to initiate connection
# Connection On Demand - Will connect when needed

Proxy Information

# Show proxy configuration
$ scutil --proxy
<dictionary> {
  ExceptionsList : <array> {
    0 : *.local
    1 : 169.254/16
  }
  FTPPassive : 1
  HTTPEnable : 0
  HTTPSEnable : 0
}

Interactive Mode

scutil has an interactive mode for exploring system configuration:

$ scutil
> list
  subKey [0] = Plugin:IPConfiguration
  subKey [1] = Plugin:InterfaceNamer
  subKey [2] = Plugin:KernelEventMonitor
  subKey [3] = Setup:
  subKey [4] = Setup:/
  ...

# Show network state
> show State:/Network/Global/IPv4
<dictionary> {
  PrimaryInterface : en0
  PrimaryService : XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
  Router : 192.168.1.1
}

# Show interface details
> show State:/Network/Interface/en0/IPv4
<dictionary> {
  Addresses : <array> {
    0 : 192.168.1.100
  }
  BroadcastAddresses : <array> {
    0 : 192.168.1.255
  }
  SubnetMasks : <array> {
    0 : 255.255.255.0
  }
}

# Show DNS state
> show State:/Network/Service/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/DNS
<dictionary> {
  ServerAddresses : <array> {
    0 : 192.168.1.1
    1 : 8.8.8.8
  }
}

# Exit interactive mode
> quit

Useful scutil One-Liners

# Get primary network interface
$ scutil --nwi | grep -A1 "Network interfaces"
Network interfaces: en0

# Get primary IPv4 address
$ scutil --nwi | grep "address" | head -1
     address : 192.168.1.100

# Check VPN status
$ scutil --nc list
Available network connection services in the current set (*=enabled):
* (Disconnected)  XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX VPN (IPSec)        "My VPN"

Routing Configuration

Viewing Routes

# Show routing table
$ netstat -rn
Routing tables

Internet:
Destination        Gateway            Flags        Netif Expire
default            192.168.1.1        UGScg          en0
127.0.0.1          127.0.0.1          UH             lo0
192.168.1          link#8             UCS            en0      !
192.168.1.1        aa:bb:cc:dd:ee:ff  UHLWIir        en0   1198
192.168.1.100/32   link#8             UCS            en0      !

# Understanding flags:
# U - Route is up
# G - Route is to a gateway
# H - Route is to a host
# S - Route was set manually
# c - Route creates new routes on use
# L - Valid protocol to link address translation
# W - Route was generated by wildcard
# I - Route requires revalidation
# r - Route was rejected

Managing Routes

# Get route to specific destination
$ route -n get google.com
   route to: 142.250.x.x
destination: default
       mask: default
    gateway: 192.168.1.1
  interface: en0
      flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>

# Add a static route
$ sudo route add -net 10.0.0.0/8 192.168.1.254
add net 10.0.0.0: gateway 192.168.1.254

# Add host route
$ sudo route add -host 10.0.0.1 192.168.1.254

# Delete a route
$ sudo route delete -net 10.0.0.0/8

# Change gateway for existing route
$ sudo route change default 192.168.1.2

# Flush routing table (use with caution)
$ sudo route flush

Persistent Routes

Routes added with route command don’t survive reboots. For persistent routes, create a launch daemon:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.static-routes</string>
    <key>ProgramArguments</key>
    <array>
        <string>/sbin/route</string>
        <string>add</string>
        <string>-net</string>
        <string>10.0.0.0/8</string>
        <string>192.168.1.254</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>WatchPaths</key>
    <array>
        <string>/Library/Preferences/SystemConfiguration</string>
    </array>
</dict>
</plist>

Save to /Library/LaunchDaemons/com.example.static-routes.plist and load with:

$ sudo launchctl load /Library/LaunchDaemons/com.example.static-routes.plist

Network Interface Creation

Creating a Bridge

# Create bridge interface
$ sudo ifconfig bridge0 create

# Add interfaces to bridge
$ sudo ifconfig bridge0 addm en0 addm en1

# Bring bridge up
$ sudo ifconfig bridge0 up

# Delete bridge
$ sudo ifconfig bridge0 destroy

Creating a VLAN Interface

# Create VLAN interface (VLAN 100 on en0)
$ sudo ifconfig vlan0 create
$ sudo ifconfig vlan0 vlan 100 vlandev en0
$ sudo ifconfig vlan0 inet 192.168.100.1 netmask 255.255.255.0
$ sudo ifconfig vlan0 up

# Or use networksetup for persistent VLAN
$ sudo networksetup -createVLAN "VLAN100" "Wi-Fi" 100

Summary

TaskRecommended ToolNotes
View/change service settingsnetworksetupPersistent, high-level
DHCP operationsipconfigRenew/release lease
Quick interface checkifconfigView status, temp changes
Hostname/DNS/proxy infoscutilSystem config access
Routingroute, netstat -rnRoute table management

Key points:

  • Use networksetup for persistent changes that should survive reboots
  • Use ipconfig for DHCP operations like renewing leases
  • Use ifconfig for quick temporary changes or diagnostics
  • Use scutil for system-level queries and hostname configuration
  • Changes made with ifconfig and route are temporary and won’t survive reboots

Understanding Network Services

macOS organizes network interfaces into “services”—a layer of abstraction that separates the physical hardware from the logical configuration. This chapter explains how network services work, how to manage them, and how to use advanced features like locations and VLANs.

Network Services Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Location: "Office"                        │
├─────────────────────────────────────────────────────────────┤
│  Service: "Wi-Fi"    │  Service: "Ethernet"  │  Service:... │
│  - Device: en0       │  - Device: en1        │              │
│  - IP: DHCP          │  - IP: Static         │              │
│  - DNS: 8.8.8.8      │  - DNS: Company       │              │
│  - Proxy: None       │  - Proxy: Yes         │              │
├─────────────────────────────────────────────────────────────┤
│                    Hardware Ports                            │
│  en0 (Wi-Fi)  │  en1 (Ethernet)  │  en2 (USB)  │  ...      │
└─────────────────────────────────────────────────────────────┘

A network service is a named configuration for a hardware port. Multiple services can exist for the same hardware port (though only one can be active). Services are grouped into locations, which are switchable sets of network configurations.

Listing Network Services

All Services

# List all network services
$ networksetup -listallnetworkservices
An asterisk (*) denotes that a network service is disabled.
Wi-Fi
USB 10/100/1000 LAN
Thunderbolt Bridge
iPhone USB
*Bluetooth PAN

# List with hardware port mapping
$ networksetup -listallhardwareports

Hardware Port: Wi-Fi
Device: en0
Ethernet Address: aa:bb:cc:dd:ee:ff

Hardware Port: USB 10/100/1000 LAN
Device: en7
Ethernet Address: bb:cc:dd:ee:ff:00

Hardware Port: Thunderbolt Bridge
Device: bridge0
Ethernet Address: cc:dd:ee:ff:00:11

Service Order and Priority

Network services have a priority order. macOS uses the highest-priority active service:

# View service order
$ networksetup -listnetworkserviceorder
An asterisk (*) denotes that a network service is disabled.
(1) Wi-Fi
(Hardware Port: Wi-Fi, Device: en0)

(2) USB 10/100/1000 LAN
(Hardware Port: USB 10/100/1000 LAN, Device: en7)

(3) Thunderbolt Bridge
(Hardware Port: Thunderbolt Bridge, Device: bridge0)

(4) iPhone USB
(Hardware Port: iPhone USB, Device: en8)

# Change service order
$ sudo networksetup -ordernetworkservices "Ethernet" "Wi-Fi" "Thunderbolt Bridge"

# After reordering
$ networksetup -listnetworkserviceorder
(1) Ethernet
(Hardware Port: USB 10/100/1000 LAN, Device: en7)

(2) Wi-Fi
(Hardware Port: Wi-Fi, Device: en0)
...

This is useful when you want Ethernet to take priority over Wi-Fi when both are connected.

Creating and Managing Services

Creating a New Service

# Create a new service on a hardware port
$ sudo networksetup -createnetworkservice "Secondary Wi-Fi" "Wi-Fi"

# Now you have two services on the same hardware:
$ networksetup -listallnetworkservices | grep -i wi-fi
Wi-Fi
Secondary Wi-Fi

Duplicating a Service

# Duplicate an existing service
$ sudo networksetup -duplicatenetworkservice "Wi-Fi" "Wi-Fi Backup"

Renaming a Service

# Rename a service
$ sudo networksetup -renamenetworkservice "Wi-Fi" "Primary Wireless"

# Verify
$ networksetup -listallnetworkservices
Primary Wireless
...

Removing a Service

# Remove a network service
$ sudo networksetup -removenetworkservice "Secondary Wi-Fi"

# Note: You cannot remove the last service on a hardware port

Enabling and Disabling Services

# Disable a service
$ sudo networksetup -setnetworkserviceenabled "Bluetooth PAN" off

# Enable a service
$ sudo networksetup -setnetworkserviceenabled "Bluetooth PAN" on

# Check if enabled
$ networksetup -listallnetworkservices
# Disabled services are marked with *
*Bluetooth PAN

Network Locations

Locations are named sets of network service configurations. They’re useful for switching between different network environments (home, office, coffee shop).

Listing Locations

# List all locations
$ networksetup -listlocations
Automatic
Home
Office
Travel

# Get current location
$ networksetup -getcurrentlocation
Automatic

Switching Locations

# Switch to a different location
$ sudo networksetup -switchtolocation "Office"

# Verify
$ networksetup -getcurrentlocation
Office

Creating and Deleting Locations

# Create a new location
$ sudo networksetup -createlocation "Coffee Shop"

# You can also create and switch in one command
$ sudo networksetup -createlocation "Hotel" populate
# 'populate' copies services from current location

# Delete a location
$ sudo networksetup -deletelocation "Coffee Shop"

# Note: Cannot delete "Automatic" or the current location

Practical Location Setup

Here’s a practical example of setting up locations:

# Create Office location with specific proxy settings
$ sudo networksetup -createlocation "Office" populate
$ sudo networksetup -switchtolocation "Office"
$ sudo networksetup -setwebproxy "Wi-Fi" proxy.company.com 8080
$ sudo networksetup -setsecurewebproxy "Wi-Fi" proxy.company.com 8080
$ sudo networksetup -setdnsservers "Wi-Fi" 10.0.0.1 10.0.0.2

# Create Home location with different settings
$ sudo networksetup -createlocation "Home" populate
$ sudo networksetup -switchtolocation "Home"
$ sudo networksetup -setwebproxystate "Wi-Fi" off
$ sudo networksetup -setsecurewebproxystate "Wi-Fi" off
$ sudo networksetup -setdnsservers "Wi-Fi" 8.8.8.8 8.8.4.4

# Switch back to Automatic for general use
$ sudo networksetup -switchtolocation "Automatic"

Automating Location Switching

You can create a script to switch locations automatically:

#!/bin/bash
# switch-network-location.sh

SSID=$(networksetup -getairportnetwork en0 | awk -F': ' '{print $2}')

case "$SSID" in
    "OfficeWiFi")
        networksetup -switchtolocation "Office"
        ;;
    "HomeNetwork")
        networksetup -switchtolocation "Home"
        ;;
    *)
        networksetup -switchtolocation "Automatic"
        ;;
esac

VLAN Configuration

VLANs (Virtual LANs) allow you to create virtual network interfaces tagged with a VLAN ID.

Creating VLANs

# List existing VLANs
$ networksetup -listVLANs
There are no VLANs currently configured on this system

# Create a VLAN
$ sudo networksetup -createVLAN "VLAN100" "Wi-Fi" 100
#                                  name     parent tag

# Verify creation
$ networksetup -listVLANs
VLAN User Defined Name: VLAN100
Parent Device: en0
Device (BSD Name): vlan0
Tag: 100

# The VLAN appears as a network service
$ networksetup -listallnetworkservices
Wi-Fi
VLAN100
...

Configuring VLAN IP

# Set VLAN to DHCP
$ sudo networksetup -setdhcp "VLAN100"

# Or set static IP
$ sudo networksetup -setmanual "VLAN100" 192.168.100.10 255.255.255.0 192.168.100.1

Deleting VLANs

# Delete a VLAN
$ sudo networksetup -deleteVLAN "VLAN100" "Wi-Fi" 100

# Verify
$ networksetup -listVLANs
There are no VLANs currently configured on this system

VLAN Use Cases

VLANs are useful for:

  • Network segmentation: Isolate traffic types (management, data, voice)
  • Testing: Connect to multiple VLANs for testing
  • Security: Separate sensitive traffic
# Example: Create management and data VLANs
$ sudo networksetup -createVLAN "Management" "Ethernet" 10
$ sudo networksetup -createVLAN "Data" "Ethernet" 20

# Configure management VLAN
$ sudo networksetup -setmanual "Management" 10.10.10.5 255.255.255.0 10.10.10.1

# Configure data VLAN with DHCP
$ sudo networksetup -setdhcp "Data"

Bond interfaces combine multiple network interfaces for increased bandwidth or redundancy.

Creating a Bond

# List available Ethernet interfaces
$ networksetup -listallhardwareports | grep -A2 "Ethernet"
Hardware Port: Ethernet 1
Device: en1

Hardware Port: Ethernet 2
Device: en2

# Create bond interface
$ sudo networksetup -createBond "BondedEthernet" en1 en2

# List bonds
$ networksetup -listBonds
BondedEthernet
Device (BSD Name): bond0
Bonded Ports: en1 en2

# Configure the bond
$ sudo networksetup -setmanual "BondedEthernet" 192.168.1.10 255.255.255.0 192.168.1.1

Bond Modes

macOS supports different bond modes (though configuration is limited from CLI):

  • Round-robin: Packets transmitted in sequential order
  • Active backup: One interface active, others standby
  • LACP (802.3ad): Link Aggregation Control Protocol

Deleting a Bond

$ sudo networksetup -deleteBond "BondedEthernet"

Network Service Details

Getting Complete Service Information

# Get all information about a service
$ networksetup -getinfo "Wi-Fi"
DHCP Configuration
IP address: 192.168.1.100
Subnet mask: 255.255.255.0
Router: 192.168.1.1
Client ID:
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Wi-Fi ID: aa:bb:cc:dd:ee:ff

MAC Address Configuration

# Get MAC (hardware) address
$ networksetup -getmacaddress "Wi-Fi"
Ethernet Address: aa:bb:cc:dd:ee:ff (Hardware Port: Wi-Fi)

# Note: macOS doesn't have a built-in way to spoof MAC addresses
# For temporary MAC spoofing (until reboot):
$ sudo ifconfig en0 ether bb:cc:dd:ee:ff:00

Additional Service Settings

# Get media status
$ networksetup -getMedia "Ethernet"
Current: autoselect
Active: 1000baseT full-duplex

# Set specific media (for Ethernet)
$ sudo networksetup -setMedia "Ethernet" 1000baseT full-duplex

# View hardware port for a service
$ networksetup -listallhardwareports | grep -A1 "Wi-Fi"
Hardware Port: Wi-Fi
Device: en0

Using scutil for Service Information

For lower-level service details, use scutil:

# Interactive mode
$ scutil
> list Setup:/Network/Service/.*
  subKey [0] = Setup:/Network/Service/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
  subKey [1] = Setup:/Network/Service/YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
  ...

> show Setup:/Network/Service/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
<dictionary> {
  IPv4 : <dictionary> {
    ConfigMethod : DHCP
  }
  IPv6 : <dictionary> {
    ConfigMethod : Automatic
  }
  Interface : <dictionary> {
    DeviceName : en0
    Hardware : AirPort
    Type : IEEE80211
    UserDefinedName : Wi-Fi
  }
  Proxies : <dictionary> {
    ExceptionsList : <array> {
      0 : *.local
      1 : 169.254/16
    }
    FTPPassive : 1
  }
}
> quit

Troubleshooting Network Services

Service Not Working

# Check if service is enabled
$ networksetup -listallnetworkservices | grep "Wi-Fi"
Wi-Fi  # No asterisk means enabled

# Check interface status
$ ifconfig en0 | grep status
	status: active

# Try removing and re-adding the service
$ sudo networksetup -removenetworkservice "Wi-Fi"
$ sudo networksetup -createnetworkservice "Wi-Fi" "Wi-Fi"
$ sudo networksetup -setdhcp "Wi-Fi"

Reset Network Configuration

# Delete current location's preferences (will recreate on restart)
$ sudo rm /Library/Preferences/SystemConfiguration/preferences.plist
$ sudo rm /Library/Preferences/SystemConfiguration/NetworkInterfaces.plist

# Reboot to regenerate
$ sudo reboot

Identify Active Service

# Find which service is providing connectivity
$ scutil --nwi
Network information

IPv4 network interface information
     en0 : flags      : 0x5 (IPv4,DNS)
           address    : 192.168.1.100
           reach      : 0x00000002 (Reachable)

   REACH : flags 0x00000002 (Reachable)

# The en0 interface tells you it's the Wi-Fi service

Summary

TaskCommand
List servicesnetworksetup -listallnetworkservices
View service ordernetworksetup -listnetworkserviceorder
Change service prioritynetworksetup -ordernetworkservices
Create servicenetworksetup -createnetworkservice
Remove servicenetworksetup -removenetworkservice
List locationsnetworksetup -listlocations
Switch locationnetworksetup -switchtolocation
Create locationnetworksetup -createlocation
Create VLANnetworksetup -createVLAN
Create bondnetworksetup -createBond

Key concepts:

  • Services are logical configurations for hardware ports
  • Locations are switchable sets of service configurations
  • VLANs allow traffic separation on the same physical interface
  • Bonds combine interfaces for bandwidth or redundancy
  • Service order determines which connection takes priority

Bonjour and mDNS

Bonjour is Apple’s implementation of zero-configuration networking (zeroconf), enabling automatic discovery of devices and services on a local network without manual configuration. It’s based on multicast DNS (mDNS) and DNS Service Discovery (DNS-SD), both open standards.

How Bonjour Works

┌─────────────────────────────────────────────────────────────┐
│                    Your Mac                                  │
│                                                             │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐      │
│  │   Safari    │   │   Finder    │   │  AirDrop    │      │
│  │ (Printers)  │   │ (Servers)   │   │             │      │
│  └──────┬──────┘   └──────┬──────┘   └──────┬──────┘      │
│         │                 │                 │              │
│         └────────────────┬┴─────────────────┘              │
│                          │                                  │
│                 ┌────────┴────────┐                        │
│                 │  mDNSResponder  │                        │
│                 │    (daemon)     │                        │
│                 └────────┬────────┘                        │
└──────────────────────────┼──────────────────────────────────┘
                           │
            ┌──────────────┴──────────────┐
            │  Multicast UDP port 5353    │
            │  224.0.0.251 (IPv4)         │
            │  ff02::fb (IPv6)            │
            └──────────────┬──────────────┘
                           │
    ┌──────────────────────┼──────────────────────┐
    │                      │                      │
┌───┴───┐            ┌────┴────┐           ┌────┴────┐
│Printer│            │Apple TV │           │  NAS    │
│       │            │         │           │         │
└───────┘            └─────────┘           └─────────┘

Key Components

mDNS (Multicast DNS): Resolves hostnames to IP addresses on the local network without a DNS server. Hostnames end in .local.

DNS-SD (DNS Service Discovery): Discovers services on the network. Services are advertised with a type (like _http._tcp) and can include metadata.

mDNSResponder: The macOS daemon that handles all mDNS and DNS-SD operations. It also functions as the system’s DNS resolver.

The .local Domain

Any hostname ending in .local is resolved via mDNS, not traditional DNS:

# These use mDNS
$ ping my-macbook.local
$ ssh server.local
$ open http://nas.local

# Your Mac's .local name
$ scutil --get LocalHostName
My-MacBook-Pro
# This makes your Mac reachable as my-macbook-pro.local

Setting Your .local Hostname

# View current Bonjour hostname
$ scutil --get LocalHostName
My-MacBook-Pro

# Change it
$ sudo scutil --set LocalHostName "work-laptop"
# Now reachable as work-laptop.local

# Verify
$ dns-sd -G v4 work-laptop.local

The dns-sd Command

dns-sd is the command-line tool for DNS Service Discovery. It can browse, register, and query services.

Browsing Services

# Browse for HTTP servers
$ dns-sd -B _http._tcp local.
Browsing for _http._tcp.local.
DATE: ---Mon 15 Jan 2024---
14:23:45.123  ...STARTING...
14:23:45.234  Add        2   8 local.               _http._tcp.          My Web Server
14:23:45.345  Add        3   8 local.               _http._tcp.          Router Admin

# Browse for SSH servers
$ dns-sd -B _ssh._tcp local.
Browsing for _ssh._tcp.local.
14:24:12.123  Add        2   8 local.               _ssh._tcp.           My-MacBook-Pro
14:24:12.234  Add        3   8 local.               server

# Browse for printers
$ dns-sd -B _ipp._tcp local.
$ dns-sd -B _printer._tcp local.

# Browse for AirPlay devices
$ dns-sd -B _airplay._tcp local.
14:25:00.123  Add        2   8 local.               _airplay._tcp.       Living Room Apple TV
14:25:00.234  Add        3   8 local.               _airplay._tcp.       Bedroom HomePod

# Browse for SMB file shares
$ dns-sd -B _smb._tcp local.

# Browse for AFP file shares (legacy)
$ dns-sd -B _afpovertcp._tcp local.

# The command runs continuously. Press Ctrl+C to stop.

Common Service Types

ServiceType
Web servers_http._tcp
Secure web_https._tcp
SSH_ssh._tcp
SFTP_sftp-ssh._tcp
SMB file sharing_smb._tcp
AFP file sharing_afpovertcp._tcp
IPP printing_ipp._tcp
AirPlay_airplay._tcp
AirDrop_airdrop._tcp
iTunes sharing_daap._tcp
Screen sharing_rfb._tcp
Remote management_eppc._tcp
Time Machine_adisk._tcp
Spotify Connect_spotify-connect._tcp

Resolving Services (Getting Details)

Once you find a service, resolve it to get its address and port:

# Resolve a specific service
$ dns-sd -L "My-MacBook-Pro" _ssh._tcp local.
Lookup My-MacBook-Pro._ssh._tcp.local.
DATE: ---Mon 15 Jan 2024---
14:30:00.123  My-MacBook-Pro._ssh._tcp.local. can be reached at My-MacBook-Pro.local.:22 (interface 8)

# The output shows hostname (My-MacBook-Pro.local.) and port (22)

Looking Up IP Addresses

# Look up IPv4 address
$ dns-sd -G v4 My-MacBook-Pro.local.
DATE: ---Mon 15 Jan 2024---
14:31:00.123  My-MacBook-Pro.local. 192.168.1.100

# Look up IPv6 address
$ dns-sd -G v6 My-MacBook-Pro.local.

# Look up both
$ dns-sd -G v4v6 My-MacBook-Pro.local.

Querying Service Records

# Query for any record
$ dns-sd -Q My-MacBook-Pro.local. any

# Query TXT records (service metadata)
$ dns-sd -Q My-MacBook-Pro._ssh._tcp.local. TXT

# Query SRV records (service location)
$ dns-sd -Q _http._tcp.local. SRV

Advertising Services

You can advertise your own services using dns-sd:

Register a Simple Service

# Advertise an SSH service
$ dns-sd -R "My SSH Server" _ssh._tcp local. 22
Registering Service My SSH Server._ssh._tcp.local. port 22
DATE: ---Mon 15 Jan 2024---
14:35:00.123  Got a reply for service My SSH Server._ssh._tcp.local.: Name now registered and active

# Advertise a web server
$ dns-sd -R "My Web Server" _http._tcp local. 8080

# The service remains advertised until you press Ctrl+C

Register Service with TXT Records

TXT records provide additional metadata about the service:

# Advertise with TXT records
$ dns-sd -R "My Web App" _http._tcp local. 8080 path=/app version=1.0

# TXT records appear as key=value pairs
# Other clients can read these to learn about the service

Practical Example: Advertise a Development Server

# Start a Python web server
$ python3 -m http.server 8000 &

# Advertise it via Bonjour
$ dns-sd -R "Dev Server" _http._tcp local. 8000 path=/ environment=development

# Now other Macs can find it in Safari's Bonjour bookmarks

Using Bonjour in Practice

Finding Printers

# Browse for all printer types
$ dns-sd -B _ipp._tcp local.
$ dns-sd -B _pdl-datastream._tcp local.
$ dns-sd -B _printer._tcp local.

# Get printer details
$ dns-sd -L "HP LaserJet" _ipp._tcp local.

Finding File Shares

# Find SMB shares (Windows/Samba/macOS)
$ dns-sd -B _smb._tcp local.
Browsing for _smb._tcp.local.
14:40:00.123  Add        2   8 local.               _smb._tcp.           NAS
14:40:00.234  Add        3   8 local.               _smb._tcp.           Macmini

# Resolve to get address
$ dns-sd -L "NAS" _smb._tcp local.
14:40:30.123  NAS._smb._tcp.local. can be reached at nas.local.:445

# Connect using the address
$ open smb://nas.local

Finding Screen Sharing Servers

# Browse for VNC/Screen Sharing
$ dns-sd -B _rfb._tcp local.
14:41:00.123  Add        2   8 local.               _rfb._tcp.           Macmini

# Connect
$ open vnc://Macmini.local

Quick Network Discovery Script

#!/bin/bash
# discover-services.sh - Quick network service discovery

echo "=== SSH Servers ==="
timeout 3 dns-sd -B _ssh._tcp local. 2>/dev/null | grep Add | awk '{print $7}'

echo -e "\n=== Web Servers ==="
timeout 3 dns-sd -B _http._tcp local. 2>/dev/null | grep Add | awk '{print $7}'

echo -e "\n=== File Shares ==="
timeout 3 dns-sd -B _smb._tcp local. 2>/dev/null | grep Add | awk '{print $7}'

echo -e "\n=== AirPlay Devices ==="
timeout 3 dns-sd -B _airplay._tcp local. 2>/dev/null | grep Add | awk '{print $7}'

mDNSResponder

mDNSResponder is the daemon that handles all Bonjour operations. It’s also macOS’s DNS resolver.

Checking mDNSResponder Status

# Check if running
$ ps aux | grep mDNSResponder
_mdnsresponder   123   0.0  0.0  4123456   7890   ??  Ss   10:00AM   0:01.23 /usr/sbin/mDNSResponder

# View mDNSResponder logs
$ log show --predicate 'process == "mDNSResponder"' --last 5m

# Real-time mDNSResponder logging
$ log stream --predicate 'process == "mDNSResponder"' --level debug

Restarting mDNSResponder

Sometimes mDNSResponder needs a restart to resolve issues:

# Restart mDNSResponder (also flushes DNS cache)
$ sudo killall -HUP mDNSResponder

# Full restart
$ sudo launchctl kickstart -k system/com.apple.mDNSResponder

DNS Cache and mDNSResponder

mDNSResponder handles both mDNS and regular DNS resolution:

# Flush DNS cache
$ sudo dscacheutil -flushcache
$ sudo killall -HUP mDNSResponder

# View current DNS configuration
$ scutil --dns

# Check DNS resolution
$ dscacheutil -q host -a name google.com

Wide-Area Bonjour

Bonjour can work beyond the local network using DNS-based Service Discovery (DNS-SD):

Browsing Wide-Area Services

# Browse services in a DNS domain
$ dns-sd -B _http._tcp example.com.

# This requires the domain to have DNS-SD records configured

Setting Up Wide-Area Bonjour

To advertise services beyond the local network, you need:

  1. A DNS server that supports dynamic updates or manually added records
  2. Service records (SRV, TXT, PTR) in your DNS zone

Example DNS records for a web service:

_http._tcp.example.com.    PTR   My\ Web\ Server._http._tcp.example.com.
My\ Web\ Server._http._tcp.example.com.  SRV   0 0 80 webserver.example.com.
My\ Web\ Server._http._tcp.example.com.  TXT   "path=/" "version=2.0"

Debugging Bonjour

Check Bonjour Registration

# See what your Mac is advertising
$ dns-sd -B _services._dns-sd._udp local.
# This lists all service types being advertised

# Then browse specific types
$ dns-sd -B _ssh._tcp local.

Network Traffic Analysis

# Capture mDNS traffic
$ sudo tcpdump -i en0 port 5353

# More detailed
$ sudo tcpdump -i en0 -v port 5353

Common Issues

Service not appearing:

# Check if mDNSResponder is running
$ pgrep mDNSResponder

# Restart if needed
$ sudo killall -HUP mDNSResponder

# Check firewall isn't blocking port 5353
$ sudo pfctl -s rules | grep 5353

Can’t resolve .local names:

# Test mDNS resolution
$ dns-sd -G v4 hostname.local.

# If failing, check network interface
$ dns-sd -G v4 hostname.local. -i en0

# Check for DNS server misconfiguration
# Some DNS servers incorrectly handle .local
$ scutil --dns | grep -A5 "resolver #1"

Programmatic Access

Using dnssd from Python

# Install: pip install pybonjour
import pybonjour

def browse_callback(sdRef, flags, interfaceIndex, errorCode, serviceName, regtype, replyDomain):
    print(f"Found: {serviceName}")

browse_sdRef = pybonjour.DNSServiceBrowse(
    regtype='_http._tcp',
    callBack=browse_callback
)

# Process events
while True:
    ready = select.select([browse_sdRef], [], [], timeout)
    if browse_sdRef in ready[0]:
        pybonjour.DNSServiceProcessResult(browse_sdRef)

Using dns-sd in Scripts

#!/bin/bash
# wait-for-service.sh - Wait for a service to appear

SERVICE_NAME="$1"
SERVICE_TYPE="${2:-_ssh._tcp}"
TIMEOUT="${3:-30}"

echo "Waiting for $SERVICE_NAME ($SERVICE_TYPE)..."

# Run dns-sd in background, capture output
dns-sd -B "$SERVICE_TYPE" local. 2>&1 | while read line; do
    if echo "$line" | grep -q "Add.*$SERVICE_NAME"; then
        echo "Found $SERVICE_NAME"
        pkill -P $$ dns-sd
        exit 0
    fi
done &

# Timeout handler
sleep $TIMEOUT
pkill -P $$ dns-sd
echo "Timeout waiting for $SERVICE_NAME"
exit 1

Summary

TaskCommand
Browse servicesdns-sd -B _type._tcp local.
Resolve servicedns-sd -L "name" _type._tcp local.
Look up IPdns-sd -G v4 hostname.local.
Advertise servicedns-sd -R "name" _type._tcp local. port
Query recordsdns-sd -Q name type
Restart mDNSRespondersudo killall -HUP mDNSResponder

Key points:

  • Bonjour enables automatic service discovery without configuration
  • mDNS resolves .local hostnames on the local network
  • DNS-SD discovers and advertises services
  • mDNSResponder handles all Bonjour operations and DNS resolution
  • dns-sd is the command-line interface to DNS Service Discovery
  • Common service types include _http._tcp, _ssh._tcp, _smb._tcp, _airplay._tcp

VPN Configuration

macOS includes built-in support for several VPN protocols and provides command-line tools for configuration and management. This chapter covers configuring VPN connections using networksetup, scutil, and managing third-party VPN solutions.

Built-in VPN Support

macOS natively supports:

ProtocolDescriptionUse Case
IKEv2Internet Key Exchange v2Modern, secure, recommended
L2TP/IPSecLayer 2 Tunneling ProtocolLegacy, widely supported
Cisco IPSecCisco proprietaryEnterprise Cisco environments
PPTPPoint-to-Point TunnelingDeprecated, insecure (removed in macOS 10.12+)

For OpenVPN, WireGuard, and other protocols, you’ll need third-party software.

Listing VPN Connections

Using scutil

# List all VPN services
$ scutil --nc list
Available network connection services in the current set (*=enabled):
* (Disconnected)   XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX VPN (IKEv2)      "Work VPN"
* (Disconnected)   YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY VPN (L2TP)       "Home VPN"
  (Disconnected)   ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ VPN (IPSec)      "Legacy VPN"

Using networksetup

# List network services (VPNs included)
$ networksetup -listallnetworkservices
An asterisk (*) denotes that a network service is disabled.
Wi-Fi
Work VPN
Home VPN

Connecting and Disconnecting VPNs

Using scutil

# Connect to VPN (by name)
$ scutil --nc start "Work VPN"

# Connect with password from stdin
$ scutil --nc start "Work VPN" --password

# Check connection status
$ scutil --nc status "Work VPN"
Connected
...

# Disconnect
$ scutil --nc stop "Work VPN"

# Show VPN statistics
$ scutil --nc show "Work VPN"

Connection Status Details

# Detailed status
$ scutil --nc status "Work VPN"
Connected
  Extended Status <dictionary> {
    IPv4 : <dictionary> {
      Addresses : <array> {
        0 : 10.0.0.100
      }
      DestAddresses : <array> {
        0 : 10.0.0.1
      }
    }
    Status : 2
  }

Monitor VPN Connection

# Watch for connection state changes
$ scutil --nc watch "Work VPN"
# Outputs status changes in real-time

Creating IKEv2 VPN Connections

IKEv2 is the recommended VPN protocol for macOS. Creating IKEv2 connections from the command line requires using networksetup and configuring through System Configuration.

Using networksetup

# Create IKEv2 VPN service
$ sudo networksetup -createnetworkservice "My IKEv2 VPN" VPN IKEv2

# Note: This creates the service but doesn't configure it
# Full configuration requires using profiles or System Preferences

IKEv2 Configuration via Profile

For complete IKEv2 configuration, use a configuration profile (.mobileconfig):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PayloadContent</key>
    <array>
        <dict>
            <key>IKEv2</key>
            <dict>
                <key>AuthenticationMethod</key>
                <string>Certificate</string>
                <key>ChildSecurityAssociationParameters</key>
                <dict>
                    <key>DiffieHellmanGroup</key>
                    <integer>14</integer>
                    <key>EncryptionAlgorithm</key>
                    <string>AES-256</string>
                    <key>IntegrityAlgorithm</key>
                    <string>SHA2-256</string>
                    <key>LifeTimeInMinutes</key>
                    <integer>1440</integer>
                </dict>
                <key>IKESecurityAssociationParameters</key>
                <dict>
                    <key>DiffieHellmanGroup</key>
                    <integer>14</integer>
                    <key>EncryptionAlgorithm</key>
                    <string>AES-256</string>
                    <key>IntegrityAlgorithm</key>
                    <string>SHA2-256</string>
                    <key>LifeTimeInMinutes</key>
                    <integer>1440</integer>
                </dict>
                <key>LocalIdentifier</key>
                <string>user@example.com</string>
                <key>RemoteAddress</key>
                <string>vpn.example.com</string>
                <key>RemoteIdentifier</key>
                <string>vpn.example.com</string>
            </dict>
            <key>PayloadType</key>
            <string>com.apple.vpn.managed</string>
            <key>PayloadIdentifier</key>
            <string>com.example.vpn.ikev2</string>
            <key>PayloadUUID</key>
            <string>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
            <key>UserDefinedName</key>
            <string>My IKEv2 VPN</string>
            <key>VPNType</key>
            <string>IKEv2</string>
        </dict>
    </array>
    <key>PayloadDisplayName</key>
    <string>VPN Configuration</string>
    <key>PayloadIdentifier</key>
    <string>com.example.vpn</string>
    <key>PayloadType</key>
    <string>Configuration</string>
    <key>PayloadUUID</key>
    <string>YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
</dict>
</plist>

Install the profile:

# Install profile
$ open my-vpn-config.mobileconfig

# Or via profiles command (requires user approval)
$ sudo profiles install -path my-vpn-config.mobileconfig

# List installed profiles
$ profiles list

# Remove profile
$ sudo profiles remove -identifier com.example.vpn

Creating L2TP/IPSec VPN Connections

L2TP/IPSec is widely supported but requires shared secret or certificate authentication.

Configuration via Profile

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PayloadContent</key>
    <array>
        <dict>
            <key>IPSec</key>
            <dict>
                <key>AuthenticationMethod</key>
                <string>SharedSecret</string>
                <key>LocalIdentifierType</key>
                <string>KeyID</string>
                <key>SharedSecret</key>
                <data>BASE64_ENCODED_SECRET</data>
            </dict>
            <key>PPP</key>
            <dict>
                <key>AuthName</key>
                <string>username</string>
                <key>CommRemoteAddress</key>
                <string>vpn.example.com</string>
            </dict>
            <key>PayloadType</key>
            <string>com.apple.vpn.managed</string>
            <key>PayloadIdentifier</key>
            <string>com.example.vpn.l2tp</string>
            <key>PayloadUUID</key>
            <string>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
            <key>UserDefinedName</key>
            <string>My L2TP VPN</string>
            <key>VPNType</key>
            <string>L2TP</string>
        </dict>
    </array>
    <key>PayloadDisplayName</key>
    <string>L2TP VPN Configuration</string>
    <key>PayloadIdentifier</key>
    <string>com.example.vpn.l2tp.config</string>
    <key>PayloadType</key>
    <string>Configuration</string>
    <key>PayloadUUID</key>
    <string>YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
</dict>
</plist>

VPN Configuration via scutil

For lower-level VPN management, use scutil interactively:

$ sudo scutil
> list
# Lists all configuration keys

> show VPN/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/IPSec
# Shows IPSec configuration for a VPN

> show VPN/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/IPv4
# Shows IPv4 configuration

OpenVPN Configuration

OpenVPN requires third-party software. The most common options are:

Tunnelblick (GUI)

# Install via Homebrew Cask
$ brew install --cask tunnelblick

# Tunnelblick uses .ovpn or .tblk configuration files
# Place configs in ~/Library/Application Support/Tunnelblick/Configurations/

OpenVPN Connect (Official Client)

# Install via Homebrew Cask
$ brew install --cask openvpn-connect

# Import .ovpn configuration through the GUI

Command-Line OpenVPN

# Install OpenVPN
$ brew install openvpn

# Connect using config file
$ sudo openvpn --config /path/to/config.ovpn

# Run in daemon mode
$ sudo openvpn --config /path/to/config.ovpn --daemon

# With authentication file
$ sudo openvpn --config /path/to/config.ovpn --auth-user-pass /path/to/auth.txt

Example OpenVPN configuration (config.ovpn):

client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
cert client.crt
key client.key
cipher AES-256-CBC
auth SHA256
comp-lzo
verb 3

OpenVPN as a Service

Create a launch daemon for automatic connection:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.openvpn</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/sbin/openvpn</string>
        <string>--config</string>
        <string>/etc/openvpn/client.ovpn</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardErrorPath</key>
    <string>/var/log/openvpn.log</string>
    <key>StandardOutPath</key>
    <string>/var/log/openvpn.log</string>
</dict>
</plist>

WireGuard Configuration

WireGuard is a modern, fast VPN protocol:

# Install WireGuard tools
$ brew install wireguard-tools

# Install WireGuard GUI (optional)
$ brew install --cask wireguard-tools

Creating WireGuard Configuration

# Generate key pair
$ wg genkey | tee privatekey | wg pubkey > publickey

# View keys
$ cat privatekey
$ cat publickey

Create configuration file (/usr/local/etc/wireguard/wg0.conf):

[Interface]
PrivateKey = YOUR_PRIVATE_KEY
Address = 10.0.0.2/24
DNS = 1.1.1.1

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

Managing WireGuard

# Bring up VPN
$ sudo wg-quick up wg0

# Check status
$ sudo wg show
interface: wg0
  public key: YOUR_PUBLIC_KEY
  private key: (hidden)
  listening port: 51820

peer: SERVER_PUBLIC_KEY
  endpoint: vpn.example.com:51820
  allowed ips: 0.0.0.0/0
  latest handshake: 1 minute, 23 seconds ago
  transfer: 123.45 MiB received, 67.89 MiB sent

# Bring down VPN
$ sudo wg-quick down wg0

WireGuard as a Service

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.wireguard.wg0</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/wg-quick</string>
        <string>up</string>
        <string>wg0</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <false/>
</dict>
</plist>

Split Tunneling

Split tunneling routes only specific traffic through the VPN.

Checking Current Routes

# View routing table
$ netstat -rn

# Check if VPN is default route
$ route -n get default
   route to: default
destination: default
       mask: default
    gateway: 10.0.0.1        # VPN gateway if all traffic routed
  interface: utun0           # VPN interface

Configuring Split Tunnel

For built-in VPNs, configure in System Preferences or via profile. For OpenVPN, modify the config:

# Route only specific networks through VPN
route 10.0.0.0 255.0.0.0
route 192.168.1.0 255.255.255.0

# Don't set VPN as default gateway
pull-filter ignore redirect-gateway

For WireGuard, modify AllowedIPs:

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = vpn.example.com:51820
# Only route these networks through VPN
AllowedIPs = 10.0.0.0/8, 192.168.1.0/24

VPN Troubleshooting

Check VPN Status

# List VPN interfaces
$ ifconfig | grep -E "^utun"
utun0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1400
utun1: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1280

# Check VPN connection status
$ scutil --nc status "Work VPN"

# View VPN-related logs
$ log show --predicate 'subsystem == "com.apple.networkextension"' --last 5m

Debug Connection Issues

# Test connectivity to VPN server
$ ping vpn.example.com
$ nc -zv vpn.example.com 500   # IKE
$ nc -zv vpn.example.com 4500  # NAT-T
$ nc -zv vpn.example.com 1194  # OpenVPN default

# Check if ports are blocked
$ sudo tcpdump -i en0 host vpn.example.com

# Verify DNS resolution
$ dig vpn.example.com

Common Issues

IKEv2 certificate errors:

# View system certificates
$ security find-certificate -a -p /Library/Keychains/System.keychain

# Import CA certificate
$ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt

L2TP connection drops:

# Check NAT-T (port 4500)
$ nc -zv vpn.example.com 4500

# May need to disable "Send all traffic over VPN" if behind NAT

DNS not working over VPN:

# Check DNS configuration
$ scutil --dns

# Flush DNS cache
$ sudo dscacheutil -flushcache
$ sudo killall -HUP mDNSResponder

# Set VPN DNS manually
$ sudo networksetup -setdnsservers "Work VPN" 10.0.0.1

Scripting VPN Connections

Auto-Connect Script

#!/bin/bash
# vpn-connect.sh - Connect to VPN with retry

VPN_NAME="Work VPN"
MAX_RETRIES=3
RETRY_DELAY=5

connect_vpn() {
    scutil --nc start "$VPN_NAME"
    sleep 3
    status=$(scutil --nc status "$VPN_NAME" | head -1)
    [ "$status" = "Connected" ]
}

for i in $(seq 1 $MAX_RETRIES); do
    echo "Attempt $i to connect to $VPN_NAME..."
    if connect_vpn; then
        echo "Connected successfully"
        exit 0
    fi
    sleep $RETRY_DELAY
done

echo "Failed to connect after $MAX_RETRIES attempts"
exit 1

Disconnect All VPNs

#!/bin/bash
# vpn-disconnect-all.sh

scutil --nc list | grep "Connected" | while read line; do
    vpn_name=$(echo "$line" | sed 's/.*"\(.*\)"/\1/')
    echo "Disconnecting $vpn_name..."
    scutil --nc stop "$vpn_name"
done

Summary

TaskCommand
List VPNsscutil --nc list
Connectscutil --nc start "VPN Name"
Disconnectscutil --nc stop "VPN Name"
Check statusscutil --nc status "VPN Name"
Show detailsscutil --nc show "VPN Name"
Monitorscutil --nc watch "VPN Name"
Install profilesudo profiles install -path config.mobileconfig

Key points:

  • IKEv2 is recommended for new deployments
  • L2TP/IPSec is widely compatible but older
  • OpenVPN and WireGuard require third-party tools
  • Configuration profiles (.mobileconfig) are the best way to deploy VPN settings
  • scutil –nc is the primary CLI tool for VPN management
  • Split tunneling routes only specific traffic through the VPN

Sharing Services via Command Line

macOS includes several built-in sharing services that are typically configured through System Preferences > Sharing. This chapter shows how to enable and configure these services from the command line, which is useful for automation, remote administration, and headless systems.

Overview of Sharing Services

macOS sharing services and their underlying technologies:

ServiceProtocolPortCLI Control
Remote Login (SSH)SSH22systemsetup, launchctl
Screen SharingVNC/ARD5900kickstart, defaults
File SharingSMB/AFP445/548sharing, defaults
Remote ManagementARD3283kickstart
Remote Apple EventsAE3031systemsetup
Printer SharingCUPS631cupsctl
Internet SharingNATvariousdefaults
Bluetooth SharingBluetooth-defaults
Content CachingHTTP49152+AssetCacheManagerUtil

Remote Login (SSH)

SSH is the most commonly enabled sharing service for command-line access.

Checking SSH Status

# Check if SSH is enabled
$ sudo systemsetup -getremotelogin
Remote Login: Off

# Or check the launchd job
$ sudo launchctl list | grep ssh
-       0       com.openssh.sshd
# "-" in PID column means not running

Enabling SSH

# Enable SSH (Remote Login)
$ sudo systemsetup -setremotelogin on
setremotelogin: Remote Login: On

# Verify it's running
$ sudo launchctl list | grep ssh
123     0       com.openssh.sshd
# PID 123 means it's running

# Test connection
$ ssh localhost

Disabling SSH

# Disable SSH
$ sudo systemsetup -setremotelogin off

# Force disable (no confirmation)
$ sudo systemsetup -f -setremotelogin off

Configuring SSH Access

By default, all administrators can SSH. To restrict access:

# Allow only specific users
$ sudo dseditgroup -o create -q com.apple.access_ssh
$ sudo dseditgroup -o edit -a username -t user com.apple.access_ssh

# Remove a user from SSH access
$ sudo dseditgroup -o edit -d username -t user com.apple.access_ssh

# Allow a group
$ sudo dseditgroup -o edit -a admin -t group com.apple.access_ssh

# Check who has access
$ sudo dseditgroup -o read com.apple.access_ssh

SSH Configuration File

SSH server configuration is at /etc/ssh/sshd_config:

# View current config
$ cat /etc/ssh/sshd_config

# Edit (carefully!)
$ sudo nano /etc/ssh/sshd_config

# After changes, restart SSH
$ sudo launchctl stop com.openssh.sshd
$ sudo launchctl start com.openssh.sshd

# Or kick the service
$ sudo launchctl kickstart -k system/com.openssh.sshd

Common SSH configuration options:

# /etc/ssh/sshd_config
Port 22
PermitRootLogin no
PasswordAuthentication yes
PubkeyAuthentication yes
AllowUsers admin developer

Screen Sharing (VNC)

Screen Sharing uses VNC protocol with Apple’s extensions.

Enabling Screen Sharing

# Enable Screen Sharing
$ sudo defaults write /var/db/launchd.db/com.apple.launchd/overrides.plist \
    com.apple.screensharing -dict Disabled -bool false

# Load the service
$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist

# Alternative: Using kickstart (for ARD, also enables Screen Sharing)
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -activate -configure -access -on -restart -agent -privs -all

Configuring Screen Sharing

# Set VNC password (for non-Apple VNC clients)
$ sudo defaults write /Library/Preferences/com.apple.VNCSettings.txt VNCPassword -data $(echo -n 'password' | xxd -p)

# Allow VNC viewers to control screen
$ sudo defaults write /Library/Preferences/com.apple.RemoteManagement VNCLegacyConnectionsEnabled -bool true

Restricting Screen Sharing Access

# Create access group
$ sudo dseditgroup -o create -q com.apple.access_screensharing

# Add user to group
$ sudo dseditgroup -o edit -a username -t user com.apple.access_screensharing

# Add admin group
$ sudo dseditgroup -o edit -a admin -t group com.apple.access_screensharing

Disabling Screen Sharing

# Disable Screen Sharing
$ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.screensharing.plist

# Or using kickstart
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -deactivate -configure -access -off

Remote Management (Apple Remote Desktop)

ARD provides more features than basic Screen Sharing.

Using kickstart

The kickstart command is the primary tool for ARD configuration:

# Full path to kickstart
KICKSTART="/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart"

# Enable Remote Management for all users
$ sudo $KICKSTART -activate -configure -access -on \
    -configure -allowAccessFor -allUsers \
    -configure -restart -agent -privs -all

# Enable for specific users with full privileges
$ sudo $KICKSTART -activate -configure -access -on \
    -configure -allowAccessFor -specifiedUsers \
    -configure -users admin,developer \
    -configure -restart -agent -privs -all

# Enable with limited privileges
$ sudo $KICKSTART -activate -configure -access -on \
    -configure -allowAccessFor -specifiedUsers \
    -configure -users helpdesk \
    -privs -ChangeSettings -TextMessages -ControlObserve -RestartShutDown

Available Privileges

# View all privilege options
$ sudo $KICKSTART -help

# Common privileges:
# -all                 All privileges
# -ControlObserve      Control and observe screen
# -TextMessages        Send text messages
# -OpenQuitApps        Open and quit applications
# -ChangeSettings      Change system settings
# -RestartShutDown     Restart or shutdown
# -CopyItems           Copy items
# -DeleteFiles         Delete files
# -GenerateReports     Generate reports

Checking ARD Status

# Check if ARD is enabled
$ sudo $KICKSTART -agent -print

# Check ARD processes
$ ps aux | grep -i "ARDAgent\|RemoteManagement"

Disabling ARD

$ sudo $KICKSTART -deactivate -configure -access -off

File Sharing (SMB/AFP)

macOS file sharing supports SMB (Windows compatible) and AFP (Apple legacy).

Using the sharing Command

# List all shares
$ sharing -l
List of Share Points
name:           Public
path:           /Users/username/Public
afp:            {
        name:   Public
}
smb:            {
        name:   Public
}

# Create a new share
$ sudo sharing -a /path/to/folder -n "MyShare"

# Create SMB-only share
$ sudo sharing -a /path/to/folder -n "SMBShare" -s 100

# Create AFP-only share
$ sudo sharing -a /path/to/folder -n "AFPShare" -a 100

# Remove a share
$ sudo sharing -r "MyShare"

Enabling File Sharing Service

# Enable SMB
$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.smbd.plist

# Check SMB status
$ sudo launchctl list | grep smbd

# Enable AFP (if needed for legacy clients)
$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.AppleFileServer.plist

SMB Configuration

# View SMB configuration
$ cat /etc/smb.conf

# Or check defaults
$ defaults read /Library/Preferences/SystemConfiguration/com.apple.smb.server

# Set workgroup
$ sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.smb.server Workgroup -string "WORKGROUP"

# Set NetBIOS name
$ sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.smb.server NetBIOSName -string "MYMAC"

# Enable guest access
$ sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.smb.server AllowGuestAccess -bool true

Managing Share Permissions

# Set share to read-only
$ sudo sharing -e "MyShare" -g 001

# Set share to read/write
$ sudo sharing -e "MyShare" -g 003

# Permission bits:
# 001 = read only
# 003 = read/write
# 000 = no access

Disabling File Sharing

# Disable SMB
$ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.smbd.plist

# Disable AFP
$ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.AppleFileServer.plist

Printer Sharing

Printer sharing uses CUPS (Common Unix Printing System).

Using cupsctl

# Enable printer sharing
$ sudo cupsctl --share-printers

# Disable printer sharing
$ sudo cupsctl --no-share-printers

# Check CUPS configuration
$ cupsctl
_debug=0
_remote_admin=0
_remote_any=0
_share_printers=1
_user_cancel_any=0

Managing Shared Printers

# List printers
$ lpstat -p -d
printer Brother_HL-L2350DW_series is idle.  enabled since Mon Jan 15 10:00:00 2024

# Share a specific printer
$ lpadmin -p "Brother_HL-L2350DW_series" -o printer-is-shared=true

# Unshare a printer
$ lpadmin -p "Brother_HL-L2350DW_series" -o printer-is-shared=false

# Allow remote access to CUPS
$ sudo cupsctl --remote-admin --remote-any

Internet Sharing

Internet Sharing shares your Mac’s internet connection with other devices.

Checking Current Configuration

# View Internet Sharing preferences
$ defaults read /Library/Preferences/SystemConfiguration/com.apple.nat

# Check NAT status
$ sudo pfctl -s nat

Enabling Internet Sharing

Internet Sharing is complex to enable from CLI; it’s easier via System Preferences. However, you can script it:

# Set up NAT sharing from en0 (Ethernet) to bridge100 (Wi-Fi hotspot)
$ sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.nat NAT -dict-add Enabled -bool true
$ sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.nat NAT -dict-add PrimaryService -string "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
$ sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.nat NAT -dict-add SharingDevices -array "bridge100"

# Load Internet Sharing
$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.InternetSharing.plist

Creating a Wi-Fi Hotspot

# Create a software access point
$ sudo networksetup -createnetworkservice "Shared Wi-Fi" Wi-Fi

# Configure the interface
$ sudo networksetup -setmanual "Shared Wi-Fi" 192.168.2.1 255.255.255.0

# Note: Full Wi-Fi hotspot creation requires additional setup
# Consider using the GUI or third-party tools for this

Content Caching

Content Caching caches Apple software updates and iCloud content for local devices.

Using AssetCacheManagerUtil

# Check status
$ sudo AssetCacheManagerUtil status
Activated: true
Active: true
CacheUsed: 12345678
CacheFree: 987654321
Port: 49152
...

# Activate content caching
$ sudo AssetCacheManagerUtil activate

# Deactivate
$ sudo AssetCacheManagerUtil deactivate

# Check what's cached
$ sudo AssetCacheManagerUtil listCaches

Content Caching Settings

# View settings
$ defaults read /Library/Preferences/com.apple.AssetCache.plist

# Set cache location
$ sudo defaults write /Library/Preferences/com.apple.AssetCache.plist CacheFolder -string "/Volumes/Cache/AssetCache"

# Set cache size limit (in bytes)
$ sudo defaults write /Library/Preferences/com.apple.AssetCache.plist CacheSizeLimit -int 107374182400  # 100GB

Remote Apple Events

Remote Apple Events allows AppleScript automation from other Macs.

# Enable Remote Apple Events
$ sudo systemsetup -setremoteappleevents on

# Disable
$ sudo systemsetup -setremoteappleevents off

# Check status
$ sudo systemsetup -getremoteappleevents

Checking All Sharing Services

Quick Status Script

#!/bin/bash
# sharing-status.sh - Check status of all sharing services

echo "=== Sharing Services Status ==="
echo

echo "Remote Login (SSH):"
sudo systemsetup -getremotelogin

echo -e "\nScreen Sharing:"
if launchctl list | grep -q "com.apple.screensharing"; then
    echo "Enabled"
else
    echo "Disabled"
fi

echo -e "\nFile Sharing (SMB):"
if launchctl list | grep -q "com.apple.smbd"; then
    echo "Enabled"
else
    echo "Disabled"
fi

echo -e "\nPrinter Sharing:"
cupsctl | grep share_printers

echo -e "\nRemote Apple Events:"
sudo systemsetup -getremoteappleevents

echo -e "\nContent Caching:"
if AssetCacheManagerUtil status 2>/dev/null | grep -q "Active: true"; then
    echo "Active"
else
    echo "Inactive"
fi

Security Considerations

Firewall Configuration

When enabling sharing services, ensure the firewall allows the traffic:

# Check application firewall status
$ /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate

# Allow incoming connections for a service
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /usr/sbin/sshd
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp /usr/sbin/sshd

Restricting Access by Network

Use the pf firewall to restrict sharing services to specific networks:

# Example: Allow SSH only from 192.168.1.0/24
# Add to /etc/pf.conf:
pass in on en0 proto tcp from 192.168.1.0/24 to any port 22
block in on en0 proto tcp from any to any port 22

Audit Logging

Enable logging for sharing service access:

# View SSH login attempts
$ log show --predicate 'process == "sshd"' --last 1h

# View Screen Sharing connections
$ log show --predicate 'subsystem == "com.apple.screensharing"' --last 1h

# View file sharing access
$ log show --predicate 'process == "smbd"' --last 1h

Summary

ServiceEnable CommandDisable Command
SSHsudo systemsetup -setremotelogin onsudo systemsetup -setremotelogin off
Screen Sharingsudo launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plistsudo launchctl unload -w ...
File Sharingsudo launchctl load -w /System/Library/LaunchDaemons/com.apple.smbd.plistsudo launchctl unload -w ...
Remote Managementsudo kickstart -activate ...sudo kickstart -deactivate ...
Printer Sharingsudo cupsctl --share-printerssudo cupsctl --no-share-printers
Content Cachingsudo AssetCacheManagerUtil activatesudo AssetCacheManagerUtil deactivate

Key points:

  • SSH (Remote Login) is the most commonly enabled service for remote management
  • Screen Sharing uses VNC with Apple extensions
  • Remote Management (ARD) provides more control than basic Screen Sharing
  • File Sharing supports both SMB (Windows) and AFP (legacy Apple)
  • Always restrict access using groups and the firewall
  • Monitor logs for unauthorized access attempts

Network Diagnostics and Troubleshooting

Effective network troubleshooting requires a methodical approach and the right tools. macOS provides both traditional Unix diagnostic utilities and Apple-specific tools. This chapter covers the essential diagnostic commands and common troubleshooting scenarios.

Diagnostic Tools Overview

Layer 7 (Application)    curl, openssl s_client, dns-sd
Layer 4 (Transport)      netstat, lsof, nc
Layer 3 (Network)        ping, traceroute, mtr, route
Layer 2 (Data Link)      arp, ndp
Layer 1 (Physical)       ifconfig, networkQuality

Cross-layer tools:       tcpdump, nettop, Wireshark

ping: Basic Connectivity Test

The most fundamental network diagnostic tool.

Basic Usage

# Ping a host (runs continuously, Ctrl+C to stop)
$ ping google.com
PING google.com (142.250.x.x): 56 data bytes
64 bytes from 142.250.x.x: icmp_seq=0 ttl=117 time=12.345 ms
64 bytes from 142.250.x.x: icmp_seq=1 ttl=117 time=11.234 ms
^C
--- google.com ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 11.234/11.789/12.345/0.556 ms

# Ping with count limit
$ ping -c 5 google.com

# Ping with specific interval (seconds)
$ ping -i 2 google.com

# Ping with timeout (seconds)
$ ping -t 5 google.com

# Ping with specific packet size
$ ping -s 1000 google.com

Advanced ping Options

# Flood ping (requires root, for testing)
$ sudo ping -f google.com

# Quiet output (only summary)
$ ping -q -c 10 google.com

# Numeric output only (no DNS resolution)
$ ping -n google.com

# Ping with record route option
$ ping -R google.com

# Specify source interface
$ ping -S 192.168.1.100 google.com

# IPv6 ping
$ ping6 ipv6.google.com

Interpreting ping Output

64 bytes from 142.250.x.x: icmp_seq=0 ttl=117 time=12.345 ms
│                          │          │        │
│                          │          │        └── Round-trip time
│                          │          └── Time-to-live (hops remaining)
│                          └── Sequence number
└── Response size

Common issues indicated by ping:

  • Request timeout: Host unreachable or filtering ICMP
  • High latency: Network congestion or routing issues
  • Variable latency: Network instability
  • Packet loss: Network problems or congestion

traceroute: Path Discovery

Traces the route packets take to reach a destination.

Basic Usage

# Trace route to host
$ traceroute google.com
traceroute to google.com (142.250.x.x), 64 hops max, 52 byte packets
 1  192.168.1.1 (192.168.1.1)  1.234 ms  1.123 ms  1.012 ms
 2  isp-gateway (10.0.0.1)  5.678 ms  5.567 ms  5.456 ms
 3  core-router.isp.net (203.0.113.1)  10.123 ms  10.012 ms  9.901 ms
 4  * * *
 5  google-peering (74.125.x.x)  12.345 ms  12.234 ms  12.123 ms
 6  142.250.x.x (142.250.x.x)  12.456 ms  12.345 ms  12.234 ms

traceroute Options

# Use ICMP instead of UDP (often more reliable)
$ sudo traceroute -I google.com

# Use TCP SYN packets (useful when ICMP blocked)
$ sudo traceroute -T -p 443 google.com

# Set maximum hops
$ traceroute -m 20 google.com

# Don't resolve hostnames (faster)
$ traceroute -n google.com

# Set packet size
$ traceroute -q 3 google.com

# Wait time for response (seconds)
$ traceroute -w 3 google.com

# Specify source interface
$ traceroute -s 192.168.1.100 google.com

Interpreting traceroute Output

 3  core-router.isp.net (203.0.113.1)  10.123 ms  10.012 ms  9.901 ms
 │  │                     │             │          │          │
 │  │                     │             └──────────┴──────────┴── Three probe times
 │  │                     └── IP address
 │  └── Hostname (if resolvable)
 └── Hop number
  • * * *: No response (could be filtering or timeout)
  • Increasing latency: Normal as distance increases
  • Sudden latency jump: Possible congestion point
  • Asymmetric routes: Different paths for request and response

mtr: Combined ping/traceroute

mtr provides real-time statistics combining ping and traceroute. Install via Homebrew:

$ brew install mtr

Using mtr

# Basic mtr (requires sudo for raw sockets)
$ sudo mtr google.com

# Report mode (non-interactive)
$ mtr -r -c 10 google.com
Start: 2024-01-15T10:00:00+0000
HOST: my-mac                      Loss%   Snt   Last   Avg  Best  Wrst StDev
  1.|-- 192.168.1.1               0.0%    10    1.2   1.3   1.0   1.8   0.2
  2.|-- isp-gateway               0.0%    10    5.6   5.7   5.4   6.2   0.3
  3.|-- core-router.isp.net       0.0%    10   10.1  10.2   9.8  10.8   0.3
  4.|-- google-peering            0.0%    10   12.3  12.4  12.1  12.9   0.2
  5.|-- 142.250.x.x               0.0%    10   12.4  12.5  12.2  13.0   0.2

# Wide report (shows both hostnames and IPs)
$ mtr -rw -c 10 google.com

# Use TCP instead of ICMP
$ sudo mtr -T -P 443 google.com

# Use UDP
$ sudo mtr -u google.com

# No DNS resolution
$ mtr -n google.com

mtr Statistics Explained

ColumnMeaning
Loss%Packet loss percentage
SntPackets sent
LastLast probe time
AvgAverage latency
BestBest (lowest) latency
WrstWorst (highest) latency
StDevStandard deviation

nettop: Real-time Network Activity

nettop shows real-time network usage by process.

Basic Usage

# Show all network activity
$ nettop

# Show TCP connections only
$ nettop -m tcp

# Show UDP connections only
$ nettop -m udp

# Show specific process
$ nettop -p Safari

# Non-interactive mode with updates
$ nettop -P -L 1

nettop Interface

                       bytes      bytes     packets    packets
process               in         out       in          out
Safari                12.3 MB    456 KB    8765        2345
Google Chrome         8.7 MB     234 KB    5678        1234
Spotify               5.4 MB     45 KB     3456        456
mDNSResponder         123 KB     23 KB     234         89

Filtering nettop Output

# Show only established connections
$ nettop -m tcp -t state=ESTABLISHED

# Show only external connections
$ nettop -m tcp -t wifi

# Show connections to specific port
$ nettop -m tcp -j port=443

tcpdump: Packet Capture

tcpdump captures and analyzes network traffic. Requires root privileges.

Basic Capture

# Capture on interface en0
$ sudo tcpdump -i en0

# Capture with more detail
$ sudo tcpdump -i en0 -v

# Even more detail
$ sudo tcpdump -i en0 -vv

# Capture specific count
$ sudo tcpdump -i en0 -c 100

# Capture all interfaces
$ sudo tcpdump -i any

Filtering Traffic

# Capture traffic to/from host
$ sudo tcpdump -i en0 host 192.168.1.50

# Capture traffic on specific port
$ sudo tcpdump -i en0 port 80
$ sudo tcpdump -i en0 port 443

# Capture TCP only
$ sudo tcpdump -i en0 tcp

# Capture UDP only
$ sudo tcpdump -i en0 udp

# Capture ICMP (ping)
$ sudo tcpdump -i en0 icmp

# Complex filters
$ sudo tcpdump -i en0 'tcp port 80 and host 192.168.1.50'
$ sudo tcpdump -i en0 'src 192.168.1.50 and dst port 443'
$ sudo tcpdump -i en0 'tcp[tcpflags] & (tcp-syn) != 0'

Saving and Reading Captures

# Save to file
$ sudo tcpdump -i en0 -w capture.pcap

# Read from file
$ tcpdump -r capture.pcap

# Read with filter
$ tcpdump -r capture.pcap port 443

# Capture with rotation (10 files, 100MB each)
$ sudo tcpdump -i en0 -w capture.pcap -C 100 -W 10

Displaying Packet Contents

# Show packet contents in hex and ASCII
$ sudo tcpdump -i en0 -X

# Show packet contents in hex only
$ sudo tcpdump -i en0 -x

# Show link-layer header
$ sudo tcpdump -i en0 -e

# Show absolute timestamps
$ sudo tcpdump -i en0 -tt

HTTP Traffic Analysis

# Capture HTTP requests
$ sudo tcpdump -i en0 -A 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

# Capture only HTTP GET requests
$ sudo tcpdump -i en0 -A 'tcp port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420'

netstat: Network Statistics

netstat shows network connections, routing tables, and statistics.

Connection Information

# Show all connections
$ netstat -an

# Show TCP connections
$ netstat -an -p tcp

# Show UDP connections
$ netstat -an -p udp

# Show listening ports
$ netstat -an | grep LISTEN

# Show established connections
$ netstat -an | grep ESTABLISHED

Routing Information

# Show routing table
$ netstat -rn
Routing tables

Internet:
Destination        Gateway            Flags        Netif Expire
default            192.168.1.1        UGScg          en0
127.0.0.1          127.0.0.1          UH             lo0
192.168.1          link#8             UCS            en0      !
192.168.1.1/32     link#8             UCS            en0      !

Statistics

# Show all protocol statistics
$ netstat -s

# Show TCP statistics
$ netstat -s -p tcp

# Show interface statistics
$ netstat -i
Name  Mtu   Network       Address            Ipkts Ierrs    Opkts Oerrs  Coll
lo0   16384 <Link#1>                        123456     0   123456     0     0
en0   1500  <Link#8>    aa:bb:cc:dd:ee:ff  987654     0   654321     0     0

lsof: List Open Files (Network)

lsof can show network connections by process.

# Show all network connections
$ lsof -i

# Show listening ports
$ lsof -i -P | grep LISTEN

# Show connections on specific port
$ lsof -i :443

# Show connections by process
$ lsof -i -c Safari

# Show connections for specific protocol
$ lsof -i TCP
$ lsof -i UDP

# Show connections to specific host
$ lsof -i @google.com

# Find process using a port
$ lsof -i :8080 -P
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
node    1234 user   23u  IPv4 0x1234      0t0  TCP *:8080 (LISTEN)

Network Quality Testing

networkQuality (macOS 12+)

# Run network quality test
$ networkQuality
==== SUMMARY ====
Upload capacity: 50.123 Mbps
Download capacity: 200.456 Mbps
Upload flows: 12
Download flows: 16
Responsiveness: High (1234 RPM)

# Sequential test (more accurate)
$ networkQuality -s

# Verbose output
$ networkQuality -v

# JSON output
$ networkQuality -c -f json

Manual Speed Test

# Download speed test
$ curl -o /dev/null -w "Speed: %{speed_download} bytes/sec\n" https://speed.cloudflare.com/__down?bytes=100000000

# Upload speed test
$ curl -X POST -o /dev/null -w "Speed: %{speed_upload} bytes/sec\n" \
    -d @/dev/zero --data-binary @<(dd if=/dev/zero bs=1M count=10 2>/dev/null) \
    https://speed.cloudflare.com/__up

DNS Diagnostics

dig

# Basic lookup
$ dig google.com

# Query specific record type
$ dig MX google.com
$ dig NS google.com
$ dig AAAA google.com    # IPv6

# Query specific DNS server
$ dig @8.8.8.8 google.com

# Trace DNS resolution
$ dig +trace google.com

# Short output
$ dig +short google.com

# Reverse lookup
$ dig -x 8.8.8.8

DNS Cache

# Flush DNS cache
$ sudo dscacheutil -flushcache
$ sudo killall -HUP mDNSResponder

# Query local cache
$ dscacheutil -q host -a name google.com

# View DNS configuration
$ scutil --dns

ARP and Neighbor Discovery

arp (IPv4)

# Show ARP table
$ arp -a
? (192.168.1.1) at aa:bb:cc:dd:ee:ff on en0 ifscope [ethernet]
? (192.168.1.100) at 11:22:33:44:55:66 on en0 ifscope permanent [ethernet]

# Delete ARP entry
$ sudo arp -d 192.168.1.50

# Add static ARP entry
$ sudo arp -s 192.168.1.50 aa:bb:cc:dd:ee:ff

ndp (IPv6)

# Show NDP table
$ ndp -an

# Delete NDP entry
$ sudo ndp -d fe80::1

# Show router advertisements
$ ndp -r

Common Troubleshooting Scenarios

“No Internet Connection”

# 1. Check physical/Wi-Fi connection
$ ifconfig en0 | grep status
	status: active

# 2. Check IP address
$ ifconfig en0 | grep "inet "
	inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255

# 3. Check gateway
$ route -n get default | grep gateway
    gateway: 192.168.1.1

# 4. Ping gateway
$ ping -c 3 192.168.1.1

# 5. Ping external IP (bypasses DNS)
$ ping -c 3 8.8.8.8

# 6. Test DNS
$ dig google.com
$ nslookup google.com

# 7. If DNS fails, flush cache
$ sudo dscacheutil -flushcache
$ sudo killall -HUP mDNSResponder

“Slow Network”

# 1. Test network quality
$ networkQuality -s

# 2. Check for packet loss
$ ping -c 100 google.com | grep "packet loss"

# 3. Check what's using bandwidth
$ nettop -P

# 4. Check for network congestion (increasing latency at specific hop)
$ mtr -r -c 20 google.com

# 5. Check interface errors
$ netstat -i | grep en0

“Can’t Connect to Specific Service”

# 1. Test DNS resolution
$ dig service.example.com

# 2. Test port connectivity
$ nc -zv service.example.com 443
Connection to service.example.com port 443 [tcp/https] succeeded!

# 3. Test with curl (HTTP/HTTPS)
$ curl -v https://service.example.com

# 4. Check if local firewall is blocking
$ sudo pfctl -s rules | grep 443

# 5. Trace route to identify where traffic stops
$ traceroute service.example.com

“Intermittent Connection Drops”

# 1. Monitor connection over time
$ ping -c 1000 gateway_ip > ping_log.txt 2>&1

# 2. Check Wi-Fi signal strength
$ /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | grep agrCtlRSSI
     agrCtlRSSI: -52

# 3. Monitor for channel interference
$ sudo tcpdump -i en0 -c 1000

# 4. Check system logs for network issues
$ log show --predicate 'subsystem == "com.apple.network"' --last 1h | grep -i error

Diagnostic Script

A comprehensive diagnostic script:

#!/bin/bash
# network-diagnostics.sh

echo "=== Network Diagnostics Report ==="
echo "Date: $(date)"
echo

echo "=== Interface Status ==="
ifconfig en0 | grep -E "status|inet "

echo -e "\n=== Gateway ==="
route -n get default | grep gateway

echo -e "\n=== DNS Servers ==="
scutil --dns | grep nameserver | head -5

echo -e "\n=== Gateway Ping ==="
ping -c 3 $(route -n get default | grep gateway | awk '{print $2}')

echo -e "\n=== External Ping (8.8.8.8) ==="
ping -c 3 8.8.8.8

echo -e "\n=== DNS Test ==="
dig +short google.com

echo -e "\n=== Traceroute (first 10 hops) ==="
traceroute -m 10 8.8.8.8

echo -e "\n=== Active Connections ==="
netstat -an | grep ESTABLISHED | head -10

echo -e "\n=== Listening Ports ==="
lsof -i -P | grep LISTEN | head -10

echo -e "\n=== Network Quality (if available) ==="
networkQuality -s 2>/dev/null || echo "networkQuality not available"

Summary

ToolPurposeCommon Usage
pingTest connectivityping -c 5 host
tracerouteTrace packet pathtraceroute host
mtrReal-time tracemtr -r host
nettopLive network monitornettop -m tcp
tcpdumpPacket capturesudo tcpdump -i en0
netstatConnection statsnetstat -an
lsof -iNetwork by processlsof -i :port
networkQualitySpeed testnetworkQuality -s
digDNS lookupdig domain
ncTest portnc -zv host port

Key troubleshooting steps:

  1. Verify physical/Wi-Fi connection (ifconfig)
  2. Check IP configuration (ipconfig, ifconfig)
  3. Test gateway connectivity (ping gateway)
  4. Test external connectivity (ping 8.8.8.8)
  5. Test DNS resolution (dig, nslookup)
  6. Trace the path (traceroute, mtr)
  7. Capture packets if needed (tcpdump)

Wi-Fi Management from Terminal

macOS provides several command-line tools for managing Wi-Fi connections, from the well-known networksetup to the hidden but powerful airport utility. This chapter covers scanning networks, connecting, diagnosing issues, and managing Wi-Fi configurations from Terminal.

Wi-Fi Tools Overview

ToolPurposeLocation
networksetupHigh-level Wi-Fi configuration/usr/sbin/networksetup
airportLow-level Wi-Fi diagnosticsHidden in framework
wdutilWireless diagnostics/usr/bin/wdutil
system_profilerHardware information/usr/sbin/system_profiler

The airport Utility

The airport command is hidden within a system framework but provides detailed Wi-Fi information and control.

Setting Up airport

# Create an alias for easy access
$ alias airport='/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'

# Or create a symbolic link
$ sudo ln -s /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport /usr/local/bin/airport

# Add to your shell profile (~/.zshrc) for persistence
echo "alias airport='/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'" >> ~/.zshrc

Current Connection Information

# Show current Wi-Fi connection details
$ airport -I
     agrCtlRSSI: -52
     agrExtRSSI: 0
    agrCtlNoise: -88
    agrExtNoise: 0
          state: running
        op mode: station
     lastTxRate: 866
        maxRate: 1200
lastAssocStatus: 0
    802.11 auth: open
      link auth: wpa2-psk
          BSSID: aa:bb:cc:dd:ee:ff
           SSID: MyNetwork
            MCS: 9
  guardInterval: 800
            NSS: 2
        channel: 149,80

Understanding airport Output

FieldDescription
agrCtlRSSISignal strength in dBm (higher is better, -50 is excellent, -80 is poor)
agrCtlNoiseNoise floor in dBm (lower is better)
stateConnection state (running, init, etc.)
lastTxRateCurrent transmit rate in Mbps
maxRateMaximum possible rate
link authAuthentication type (wpa2-psk, wpa3, etc.)
BSSIDMAC address of the access point
SSIDNetwork name
channelChannel number (and width for 802.11ac/ax)
NSSNumber of spatial streams

Signal Quality Assessment

# Quick signal strength check
$ airport -I | grep agrCtlRSSI
     agrCtlRSSI: -52

# Signal-to-Noise Ratio (SNR)
$ airport -I | grep -E "agrCtlRSSI|agrCtlNoise"
     agrCtlRSSI: -52
    agrCtlNoise: -88
# SNR = RSSI - Noise = -52 - (-88) = 36 dB (good)

Signal strength guidelines:

RSSI (dBm)QualityTypical Use
-30 to -50ExcellentAny application
-50 to -60GoodReliable for most uses
-60 to -70FairWeb browsing, email
-70 to -80WeakMay have issues
-80 to -90Very WeakLikely unreliable

Scanning for Networks

# Scan for available Wi-Fi networks
$ airport -s
                            SSID BSSID             RSSI CHANNEL HT CC SECURITY
                       MyNetwork aa:bb:cc:dd:ee:ff -52  149,+1  Y  US WPA2(PSK/AES/AES)
                   Neighbor_WiFi bb:cc:dd:ee:ff:00 -68  6       Y  US WPA2(PSK/AES/AES)
                     Guest_Wifi  cc:dd:ee:ff:00:11 -75  11      Y  -- WPA2(PSK/AES/AES)
                   HiddenNetwork dd:ee:ff:00:11:22 -80  36,+1   Y  US WPA3(SAE/AES/AES)
                        OpenWiFi ee:ff:00:11:22:33 -62  1       N  -- NONE

# Scan with more details (XML output)
$ airport -s -x

Understanding Scan Output

ColumnDescription
SSIDNetwork name
BSSIDAccess point MAC address
RSSISignal strength in dBm
CHANNELChannel (with width indicator for AC/AX)
HTHigh Throughput (802.11n+) capable
CCCountry code
SECURITYSecurity protocol

Channel notation:

  • 6 - Channel 6, 20MHz width
  • 149,+1 - Channel 149, 40MHz bonded with upper channel
  • 36,-1 - Channel 36, 40MHz bonded with lower channel
  • 149,80 - Channel 149, 80MHz width

Disconnecting from Wi-Fi

# Disconnect from current network
$ sudo airport -z

# Verify disconnection
$ airport -I | grep SSID
# (no output means disconnected)

Supported Channels

# Show supported channels for your Wi-Fi adapter
$ airport -c
Supported channels:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 149, 153, 157, 161, 165

networksetup for Wi-Fi

networksetup provides high-level Wi-Fi management.

Wi-Fi Power Control

# Check Wi-Fi power state
$ networksetup -getairportpower en0
Wi-Fi Power (en0): On

# Turn Wi-Fi off
$ networksetup -setairportpower en0 off

# Turn Wi-Fi on
$ networksetup -setairportpower en0 on

Connecting to Networks

# Connect to a network (with password)
$ networksetup -setairportnetwork en0 "NetworkName" "password"

# Connect to open network
$ networksetup -setairportnetwork en0 "OpenNetwork"

# Get current network
$ networksetup -getairportnetwork en0
Current Wi-Fi Network: MyNetwork

Managing Preferred Networks

Preferred networks are automatically connected to when in range:

# List preferred (known) networks
$ networksetup -listpreferredwirelessnetworks en0
Preferred networks on en0:
	MyHomeNetwork
	OfficeNetwork
	CoffeeShopWiFi

# Add a preferred network
$ networksetup -addpreferredwirelessnetworkatindex en0 "NewNetwork" 0 WPA2 "password"
#                                                   interface SSID    index security password

# Remove a preferred network
$ sudo networksetup -removepreferredwirelessnetwork en0 "CoffeeShopWiFi"
Removed CoffeeShopWiFi from the preferred networks list

# Remove all preferred networks
$ sudo networksetup -removeallpreferredwirelessnetworks en0

Preferred Network Order

# Networks are tried in order; index 0 has highest priority
# Add network at index 0 (highest priority)
$ networksetup -addpreferredwirelessnetworkatindex en0 "PriorityNetwork" 0 WPA2 "password"

wdutil: Wireless Diagnostics

wdutil provides additional diagnostic capabilities:

# Show wireless diagnostics info
$ sudo wdutil info

# Capture wireless diagnostic information
$ sudo wdutil diagnose

# Log wireless events
$ sudo wdutil log

# Note: wdutil requires sudo for most operations

The diagnose command creates a diagnostic report that can be useful for troubleshooting.

System Information

system_profiler

# Detailed Wi-Fi hardware info
$ system_profiler SPAirPortDataType
Wi-Fi:

      Software Versions:
          CoreWLAN: 16.0 (1657)
          CoreWLANKit: 16.0 (1657)
          Menu Extra: 17.0 (1728)
          IO80211 Family: 12.0 (1200.12.2)
          Diagnostics: 11.0 (1163)
          AirPort Utility: 6.3.9 (639.15)
      Interfaces:
        en0:
          Card Type: Wi-Fi  (0x14E4, 0x4387)
          Firmware Version: wl0: Jul 12 2023 02:42:47 version 20.10.1062.3.8.7.158 FWID 01-...
          Locale: ETSI
          Country Code: US
          Supported PHY Modes: 802.11 a/b/g/n/ac/ax
          Supported Channels: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 149, 153, 157, 161, 165
          Status: Connected
          Current Network Information:
            MyNetwork:
              PHY Mode: 802.11ax
              Channel: 149 (5 GHz, 80 MHz)
              Network Type: Infrastructure
              Security: WPA2 Personal
              Signal / Noise: -52 dBm / -88 dBm
              Transmit Rate: 1201
              MCS Index: 11

Wi-Fi Diagnostics and Troubleshooting

Check Wi-Fi Status

#!/bin/bash
# wifi-status.sh - Comprehensive Wi-Fi status

echo "=== Wi-Fi Status ==="

# Check if Wi-Fi is on
power=$(networksetup -getairportpower en0 | awk '{print $4}')
echo "Power: $power"

if [ "$power" = "On" ]; then
    # Get current network
    network=$(networksetup -getairportnetwork en0 | cut -d: -f2 | xargs)
    echo "Network: $network"

    # Get signal info
    airport -I | grep -E "agrCtlRSSI|agrCtlNoise|lastTxRate|channel"

    # Calculate SNR
    rssi=$(airport -I | grep agrCtlRSSI | awk '{print $2}')
    noise=$(airport -I | grep agrCtlNoise | awk '{print $2}')
    snr=$((rssi - noise))
    echo "Signal-to-Noise Ratio: $snr dB"
fi

Monitor Wi-Fi Signal

# Continuous signal monitoring
$ while true; do
    airport -I | grep agrCtlRSSI
    sleep 1
done

# Or with timestamp
$ while true; do
    echo "$(date +%H:%M:%S) - $(airport -I | grep agrCtlRSSI | awk '{print $2}') dBm"
    sleep 1
done

Find Best Channel

#!/bin/bash
# best-channel.sh - Find least congested Wi-Fi channels

echo "=== Channel Usage Analysis ==="
echo "Scanning networks..."

# Count networks per channel
airport -s | tail -n +2 | awk '{print $4}' | cut -d, -f1 | sort | uniq -c | sort -rn

echo -e "\n2.4 GHz Channels (1, 6, 11 recommended):"
airport -s | tail -n +2 | awk '$4 <= 14 {print $4}' | sort | uniq -c

echo -e "\n5 GHz Channels:"
airport -s | tail -n +2 | awk '$4 > 14 {print $4}' | cut -d, -f1 | sort | uniq -c

Troubleshooting Connection Issues

# 1. Check if Wi-Fi hardware is available
$ networksetup -listallhardwareports | grep -A1 "Wi-Fi"
Hardware Port: Wi-Fi
Device: en0

# 2. Check if interface is up
$ ifconfig en0 | grep status
	status: active

# 3. Check signal strength
$ airport -I | grep agrCtlRSSI
     agrCtlRSSI: -52

# 4. Check for IP address
$ ifconfig en0 | grep "inet "
	inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255

# 5. If no IP, try renewing DHCP
$ sudo ipconfig set en0 DHCP

# 6. Check DNS
$ scutil --dns | grep "nameserver"

# 7. Test connectivity
$ ping -c 3 8.8.8.8

Reset Wi-Fi Configuration

When all else fails:

# Turn Wi-Fi off and on
$ networksetup -setairportpower en0 off
$ sleep 2
$ networksetup -setairportpower en0 on

# Remove and reconnect to network
$ sudo networksetup -removepreferredwirelessnetwork en0 "ProblemNetwork"
$ networksetup -setairportnetwork en0 "ProblemNetwork" "password"

# Full Wi-Fi reset (removes all preferences)
$ sudo rm -f /Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist
$ sudo rm -f /Library/Preferences/com.apple.wifi.message-tracer.plist
# Then reboot

Captive Portal Networks

Captive portals (hotel, coffee shop Wi-Fi) sometimes cause issues:

# Force captive portal check
$ /System/Library/CoreServices/Captive\ Network\ Assistant.app/Contents/MacOS/Captive\ Network\ Assistant

# Or manually open the captive portal
$ open http://captive.apple.com

Wi-Fi Logging

Enable Wi-Fi Debug Logging

# Enable debug logging
$ sudo defaults write /Library/Preferences/com.apple.wifi.message-tracer.plist LogLevel -int 5

# View Wi-Fi logs
$ log show --predicate 'subsystem == "com.apple.wifi"' --last 5m

# Stream Wi-Fi logs in real-time
$ log stream --predicate 'subsystem == "com.apple.wifi"' --level debug

# Disable debug logging when done
$ sudo defaults delete /Library/Preferences/com.apple.wifi.message-tracer.plist LogLevel

Wireless Diagnostics Report

# Generate comprehensive wireless diagnostic report
$ sudo wdutil diagnose

# Report is saved to /var/tmp/
# Look for WirelessDiagnostics*.tar.gz

Automation Scripts

Auto-Connect Script

#!/bin/bash
# auto-connect.sh - Connect to first available preferred network

INTERFACE="en0"
PREFERRED_NETWORKS=("HomeNetwork" "OfficeNetwork" "BackupNetwork")
PASSWORDS=("homepass" "officepass" "backuppass")

# Check if already connected
current=$(networksetup -getairportnetwork $INTERFACE | cut -d: -f2 | xargs)
if [ -n "$current" ]; then
    echo "Already connected to: $current"
    exit 0
fi

# Scan and connect
echo "Scanning for networks..."
available=$(airport -s | tail -n +2 | awk '{print $1}')

for i in "${!PREFERRED_NETWORKS[@]}"; do
    network="${PREFERRED_NETWORKS[$i]}"
    if echo "$available" | grep -q "^${network}$"; then
        echo "Connecting to $network..."
        networksetup -setairportnetwork $INTERFACE "$network" "${PASSWORDS[$i]}"
        sleep 3
        if networksetup -getairportnetwork $INTERFACE | grep -q "$network"; then
            echo "Connected successfully!"
            exit 0
        fi
    fi
done

echo "No preferred networks available"
exit 1

Signal Monitor Script

#!/bin/bash
# signal-monitor.sh - Monitor and log Wi-Fi signal over time

LOGFILE="/tmp/wifi-signal.log"
INTERVAL=5

echo "Monitoring Wi-Fi signal (logging to $LOGFILE)..."
echo "Timestamp,SSID,RSSI,Noise,SNR,TxRate,Channel" > $LOGFILE

while true; do
    info=$(airport -I)
    ssid=$(echo "$info" | grep " SSID:" | awk '{print $2}')
    rssi=$(echo "$info" | grep "agrCtlRSSI:" | awk '{print $2}')
    noise=$(echo "$info" | grep "agrCtlNoise:" | awk '{print $2}')
    txrate=$(echo "$info" | grep "lastTxRate:" | awk '{print $2}')
    channel=$(echo "$info" | grep "channel:" | awk '{print $2}')
    snr=$((rssi - noise))

    timestamp=$(date "+%Y-%m-%d %H:%M:%S")
    echo "$timestamp,$ssid,$rssi,$noise,$snr,$txrate,$channel" >> $LOGFILE
    echo "$timestamp - SSID: $ssid, RSSI: $rssi dBm, SNR: $snr dB, Rate: $txrate Mbps"

    sleep $INTERVAL
done

Network Switcher

#!/bin/bash
# wifi-switch.sh - Quick network switching

case "$1" in
    home)
        networksetup -setairportnetwork en0 "HomeNetwork" "homepassword"
        ;;
    office)
        networksetup -setairportnetwork en0 "OfficeNetwork" "officepassword"
        ;;
    hotspot)
        networksetup -setairportnetwork en0 "iPhone" "hotspotpassword"
        ;;
    off)
        networksetup -setairportpower en0 off
        ;;
    on)
        networksetup -setairportpower en0 on
        ;;
    scan)
        airport -s
        ;;
    status)
        airport -I
        ;;
    *)
        echo "Usage: $0 {home|office|hotspot|off|on|scan|status}"
        exit 1
        ;;
esac

Summary

TaskCommand
Turn Wi-Fi on/offnetworksetup -setairportpower en0 on/off
Current connection infoairport -I
Scan for networksairport -s
Connect to networknetworksetup -setairportnetwork en0 "SSID" "password"
Disconnectsudo airport -z
Current networknetworksetup -getairportnetwork en0
List preferred networksnetworksetup -listpreferredwirelessnetworks en0
Remove preferred networknetworksetup -removepreferredwirelessnetwork en0 "SSID"
Check signal strengthairport -I | grep agrCtlRSSI
Show supported channelsairport -c
Wi-Fi hardware infosystem_profiler SPAirPortDataType

Key points:

  • airport is the hidden power tool for Wi-Fi diagnostics
  • networksetup handles high-level configuration and connections
  • Signal strength (RSSI) and Signal-to-Noise Ratio (SNR) are key metrics
  • Preferred networks determine automatic connection priority
  • Debug logging can be enabled for troubleshooting
  • Channel analysis helps optimize Wi-Fi performance

Security on macOS

macOS employs a multi-layered security model that goes far beyond traditional Unix permissions. While it retains the foundational user/group/world permission system from its BSD heritage, Apple has built an extensive security architecture on top, including code signing, sandboxing, encrypted storage, and hardware-backed security features. Understanding these layers is essential for anyone administering macOS systems or developing software for the platform.

The Security Philosophy

Apple’s approach to macOS security follows a defense-in-depth strategy:

┌─────────────────────────────────────────────────────────────────┐
│                    Hardware Security                             │
│          (Secure Enclave, T2/Apple Silicon, Secure Boot)        │
├─────────────────────────────────────────────────────────────────┤
│                    Disk Encryption                               │
│                      (FileVault 2)                               │
├─────────────────────────────────────────────────────────────────┤
│                    Kernel Protection                             │
│            (SIP, Kernel Extension Signing, KTRR)                │
├─────────────────────────────────────────────────────────────────┤
│                    Application Security                          │
│      (Gatekeeper, Notarization, Sandboxing, Hardened Runtime)   │
├─────────────────────────────────────────────────────────────────┤
│                    Privacy Controls                              │
│          (TCC, Privacy Preferences, Transparency)               │
├─────────────────────────────────────────────────────────────────┤
│                    Traditional Unix Security                     │
│          (Users, Groups, Permissions, ACLs)                     │
└─────────────────────────────────────────────────────────────────┘

Each layer provides protection even if another layer is compromised.

Key Security Components

Code Signing and Trust

macOS verifies that software comes from identified developers:

ComponentPurpose
Code SigningCryptographically verifies software identity
GatekeeperEnforces signature requirements for app execution
NotarizationApple’s automated security check for distributed software
Hardened RuntimeRestricts dangerous operations in signed code

Privacy and Sandboxing

Applications are isolated and must request permission for sensitive operations:

ComponentPurpose
App SandboxRestricts app file system and resource access
TCC (Transparency, Consent, Control)Manages privacy permissions
EntitlementsDeclares capabilities an app needs

Hardware-Backed Security

Modern Macs include dedicated security hardware:

ComponentPurpose
Secure EnclaveIsolated processor for cryptographic operations
T2 / Apple SiliconSecure boot, encryption keys, biometrics
Touch IDBiometric authentication

Command-Line Security Tools

macOS provides extensive security tools for the terminal:

Code Signing and Verification

# Verify an application's signature
$ codesign -dv --verbose=4 /Applications/Safari.app

# Check Gatekeeper assessment
$ spctl --assess -v /Applications/Safari.app

# View extended attributes (quarantine)
$ xattr -l ~/Downloads/installer.pkg

Keychain and Credentials

# List keychains
$ security list-keychains

# Find a password
$ security find-generic-password -a "account" -s "service" -w

# Add a certificate to keychain
$ security add-certificates /path/to/cert.cer

Privacy and TCC

# Reset privacy permissions for an app
$ tccutil reset Camera com.example.app

# Check privacy database (requires Full Disk Access)
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT * FROM access"

Disk Encryption

# Check FileVault status
$ fdesetup status
FileVault is On.

# List enabled users
$ fdesetup list

# Check encryption progress
$ diskutil apfs list | grep -A5 "FileVault"

System Security

# Check SIP status
$ csrutil status

# Check Gatekeeper status
$ spctl --status

# View firmware password status
$ sudo firmwarepasswd -check

Quick Security Audit

A rapid assessment of system security posture:

# Check SIP
$ csrutil status

# Check FileVault
$ fdesetup status

# Check Gatekeeper
$ spctl --status

# Check firewall
$ /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate

# Check auto-login status
$ defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null || echo "Auto-login disabled"

# Check remote access services
$ sudo systemsetup -getremotelogin
$ sudo launchctl list | grep -E "screensharing|vnc|ARD"

Security on Apple Silicon vs Intel

Apple Silicon Macs have enhanced security features:

FeatureIntel (T2)Intel (No T2)Apple Silicon
Secure BootYesNoYes
Hardware EncryptionYesSoftware onlyYes
Secure EnclaveYesNoYes
Boot Security ModesLimitedNoFull
Kernel Extension LoadingRestrictedAllowedVery Restricted

What You’ll Learn in This Part

Gatekeeper and Code Signing explains how macOS verifies software authenticity using codesign, spctl, and the quarantine system, including how to sign your own tools and scripts.

Notarization Requirements covers Apple’s notarization process for distributed software, using xcrun notarytool and stapling tickets to your applications.

App Sandboxing explores how sandboxing isolates applications, including the sandbox-exec command, sandbox profiles, and entitlements.

Keychain Services from Terminal shows how to manage passwords, certificates, and keys using the security command for automation and scripting.

FileVault and Disk Encryption covers full-disk encryption management using fdesetup, including enabling, key management, and recovery scenarios.

Privacy Controls and TCC Database explains the Transparency, Consent, and Control system, including tccutil and programmatic permission handling.

Secure Boot and T2/Apple Silicon details hardware security features, boot security policies, and firmware password configuration.

Security Best Practices provides a comprehensive hardening checklist and ongoing security monitoring strategies.

Common Security Tasks

Allow an App Blocked by Gatekeeper

# Remove quarantine attribute
$ xattr -d com.apple.quarantine /path/to/app

# Or add to Gatekeeper whitelist
$ spctl --add --label "Approved" /path/to/app

Securely Store a Password in Script

# Store in keychain
$ security add-generic-password -a "$USER" -s "myservice" -w "secret"

# Retrieve in script
PASSWORD=$(security find-generic-password -a "$USER" -s "myservice" -w)

Check If an App Is Notarized

$ spctl -a -vvv /Applications/SomeApp.app
/Applications/SomeApp.app: accepted
source=Notarized Developer ID

Grant Terminal Full Disk Access

  1. Open System Preferences > Privacy & Security > Full Disk Access
  2. Click the lock to make changes
  3. Add Terminal.app (or your terminal emulator)
  4. Restart Terminal

This is required for many security-related terminal operations.

The following chapters provide in-depth coverage of each security component, with practical examples for both security auditing and system hardening.

Gatekeeper and Code Signing

Code signing is the foundation of macOS application security. Every application, framework, plugin, and script that runs on modern macOS should be signed to verify its authenticity and integrity. Gatekeeper enforces these requirements, blocking unsigned or improperly signed software from running. Understanding code signing is essential for developers distributing software and administrators managing which applications can run.

How Code Signing Works

Code signing creates a cryptographic seal over an application’s contents:

┌──────────────────────────────────────────────────────────────────┐
│                     Signed Application                            │
├──────────────────────────────────────────────────────────────────┤
│  Code Directory                                                   │
│  ├── Hash of each code page                                      │
│  ├── Hash of Info.plist                                          │
│  ├── Hash of embedded resources                                  │
│  └── Hash of entitlements                                        │
├──────────────────────────────────────────────────────────────────┤
│  CMS Signature                                                    │
│  ├── Developer certificate                                       │
│  ├── Certificate chain to Apple Root CA                          │
│  └── Digital signature of Code Directory                         │
└──────────────────────────────────────────────────────────────────┘

When macOS loads signed code, it verifies:

  1. The signature matches the code content
  2. The signing certificate is valid
  3. The certificate chains to a trusted root
  4. The certificate hasn’t been revoked

The codesign Command

Examining Signatures

View basic signature information:

# Basic signature check
$ codesign -v /Applications/Safari.app
/Applications/Safari.app: valid on disk

# Detailed signature information
$ codesign -dv /Applications/Safari.app
Executable=/Applications/Safari.app/Contents/MacOS/Safari
Identifier=com.apple.Safari
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=1012 flags=0x10000(runtime) hashes=21+7 location=embedded
Signature size=4442
Authority=Apple Mac OS Application Signing
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
Timestamp=Jan 15, 2024 at 2:30:00 PM
Info.plist entries=35
TeamIdentifier=not applicable
Runtime Version=14.0.0
Sealed Resources version=2 rules=2 files=0
Internal requirements count=1 size=64

# Very verbose output
$ codesign -dv --verbose=4 /Applications/Safari.app

# Display signing requirements
$ codesign -dr - /Applications/Safari.app

Examining Entitlements

Entitlements define what a signed application is allowed to do:

# View entitlements
$ codesign -d --entitlements - /Applications/Safari.app
Executable=/Applications/Safari.app/Contents/MacOS/Safari
[Dict]
    [Key] com.apple.private.webkit.webinspector.allow
    [Value]
        [Bool] true
    [Key] com.apple.security.app-sandbox
    [Value]
        [Bool] true
    ...

# Extract entitlements to XML
$ codesign -d --entitlements :- /Applications/Safari.app > entitlements.plist

# View as XML (more readable)
$ codesign -d --entitlements - --xml /Applications/Safari.app | plutil -convert xml1 -o - -

Verifying Signatures

# Quick verification
$ codesign -v /Applications/Safari.app
/Applications/Safari.app: valid on disk

# Verify at a deeper level
$ codesign -vv /Applications/Safari.app
/Applications/Safari.app: valid on disk
satisfies its Designated Requirement

# Strict verification (checks all resources)
$ codesign --verify --strict /Applications/Safari.app

# Verbose verification with details
$ codesign --verify --verbose=4 /Applications/Safari.app

Signing Your Own Code

For ad-hoc signing (no Apple developer account):

# Sign a binary for local use (ad-hoc signature)
$ codesign -s - /path/to/mybinary

# Sign with specific identifier
$ codesign -s - -i com.example.mytool /path/to/mybinary

# Force re-sign (overwrite existing signature)
$ codesign -f -s - /path/to/mybinary

With a Developer ID certificate:

# List available signing identities
$ security find-identity -v -p codesigning
  1) ABC123... "Developer ID Application: Your Name (TEAMID)"
  2) DEF456... "Apple Development: your@email.com (TEAMID)"
     2 valid identities found

# Sign with Developer ID
$ codesign -s "Developer ID Application: Your Name (TEAMID)" \
  --timestamp \
  --options runtime \
  /path/to/MyApp.app

# Sign with specific entitlements
$ codesign -s "Developer ID Application: Your Name (TEAMID)" \
  --timestamp \
  --options runtime \
  --entitlements entitlements.plist \
  /path/to/MyApp.app

Signing Options

# Enable hardened runtime (required for notarization)
$ codesign -s "Developer ID Application" \
  --options runtime \
  /path/to/app

# Available options (can be combined with comma)
# runtime    - Enable hardened runtime
# library    - Library validation
# kill       - Kill process on signature invalidation
# hard       - Hard library validation

# Add timestamp (proves when signing occurred)
$ codesign -s "Developer ID Application" \
  --timestamp \
  /path/to/app

# Sign preserving other signatures
$ codesign -s "Developer ID Application" \
  --preserve-metadata=identifier,entitlements,flags \
  /path/to/app

Removing Signatures

# Remove signature from a binary
$ codesign --remove-signature /path/to/binary

# This is sometimes needed before re-signing
$ codesign --remove-signature /path/to/app && \
  codesign -s "Developer ID Application" /path/to/app

Gatekeeper

Gatekeeper is the macOS subsystem that enforces code signing policy at launch time.

Checking Gatekeeper Status

# Check if Gatekeeper is enabled
$ spctl --status
assessments enabled

# If disabled
$ spctl --status
assessments disabled

Managing Gatekeeper

# Disable Gatekeeper (requires admin, not recommended)
$ sudo spctl --master-disable

# Enable Gatekeeper
$ sudo spctl --master-enable

Gatekeeper Assessments

# Assess an application
$ spctl --assess -v /Applications/Safari.app
/Applications/Safari.app: accepted
source=Apple System

# Assess with type specification
$ spctl --assess --type execute -v /Applications/SomeApp.app
/Applications/SomeApp.app: accepted
source=Notarized Developer ID

# Possible results:
# accepted - Allowed to run
# rejected - Blocked by Gatekeeper
# source values: Apple System, Apple, Notarized Developer ID, Developer ID, etc.

# Check an installer package
$ spctl --assess --type install -v /path/to/installer.pkg

# Detailed rejection reason
$ spctl -a -t exec -vvv /path/to/app.app

Adding Rules

You can create custom Gatekeeper rules:

# Add an application to the whitelist
$ spctl --add --label "Approved" /path/to/app.app

# Add by hash (more secure)
$ spctl --add --hash $(codesign -dv --verbose=2 /path/to/app 2>&1 | grep CDHash | cut -d= -f2)

# Add by requirement
$ spctl --add --requirement 'anchor apple generic and certificate leaf[subject.CN] = "Developer ID"'

# List current rules
$ spctl --list

# Remove a rule
$ spctl --remove --label "Approved"
# Check kext consent status
$ spctl kext-consent status
Kernel Extension User Consent: ENABLED

# List approved team IDs
$ spctl kext-consent list

# Add a team ID (requires Recovery Mode)
$ sudo spctl kext-consent add TEAMID123

# Disable kext consent (requires Recovery Mode)
$ spctl kext-consent disable

The Quarantine System

When you download files from the internet, macOS adds a quarantine extended attribute that triggers Gatekeeper assessment on first launch.

Viewing Quarantine Attributes

# Check if a file is quarantined
$ xattr /path/to/downloaded.app
com.apple.quarantine

# View quarantine attribute details
$ xattr -p com.apple.quarantine /path/to/downloaded.app
0083;65a12345;Safari;12345678-1234-1234-1234-123456789012

# Format: flags;timestamp_hex;application;UUID
# Flags:
#   0001 = downloaded
#   0002 = do not trigger assessment
#   0040 = user approved (Gatekeeper OK)
#   0080 = App Translocation applied

# View all extended attributes
$ xattr -l /path/to/downloaded.app

Managing Quarantine

# Remove quarantine (bypass Gatekeeper assessment)
$ xattr -d com.apple.quarantine /path/to/downloaded.app

# Remove quarantine recursively from an app bundle
$ xattr -dr com.apple.quarantine /path/to/MyApp.app

# Check if quarantine attribute exists before removing
$ xattr /path/to/file | grep -q quarantine && \
  xattr -d com.apple.quarantine /path/to/file

# Add quarantine (for testing)
$ xattr -w com.apple.quarantine "0001;$(printf '%x' $(date +%s));Terminal;12345678-1234-1234-1234-123456789012" /path/to/file

App Translocation

When a quarantined app is run from certain locations (like Downloads), macOS may run it from a randomized read-only path (App Translocation):

# Check if an app is translocated
$ xattr -p com.apple.quarantine /path/to/app
# Look for 0080 flag

# Apps in these locations may be translocated:
# - ~/Downloads
# - Any location opened from a quarantined disk image

# To prevent translocation, move to /Applications:
$ mv ~/Downloads/MyApp.app /Applications/

# Or remove quarantine attribute
$ xattr -dr com.apple.quarantine ~/Downloads/MyApp.app

Code Signing for Scripts

Scripts can be signed too, though it’s less common:

# Sign a shell script
$ codesign -s - /path/to/script.sh

# Verify script signature
$ codesign -v /path/to/script.sh

# Sign with identifier
$ codesign -s - -i com.example.myscript /path/to/script.sh

For Python scripts packaged as applications:

# After using py2app or similar
$ codesign -s "Developer ID Application" \
  --deep \
  --strict \
  --options runtime \
  /path/to/MyPythonApp.app

Certificate Types

Different certificates serve different purposes:

Certificate TypePurposeDistribution
Apple DevelopmentTesting on your devicesNot distributable
Apple DistributionApp Store submissionApp Store only
Developer ID ApplicationDirect distributionOutside App Store
Developer ID InstallerSigned packagesOutside App Store
# View certificate details
$ security find-certificate -c "Developer ID" -p | openssl x509 -noout -text

# List all code signing certificates
$ security find-identity -v -p codesigning

Troubleshooting Code Signing

Common Errors

# "not signed at all"
$ codesign -v unsigned.app
unsigned.app: code object is not signed at all

# Solution: Sign the application
$ codesign -s - unsigned.app

# "a sealed resource is missing or invalid"
$ codesign -v damaged.app
damaged.app: a sealed resource is missing or invalid

# Check what resources are problematic
$ codesign --verify --verbose=4 damaged.app 2>&1 | grep -i resource

# Solution: Re-sign after ensuring all resources are present
$ codesign -f -s "Developer ID Application" damaged.app

# "the signature is invalid"
# Usually means the binary was modified after signing
$ codesign --remove-signature app.app
$ codesign -s "Developer ID Application" app.app

Deep Signing

For app bundles with nested code:

# Sign nested components first, then the main app
$ codesign -s "Developer ID Application" \
  MyApp.app/Contents/Frameworks/Helper.framework

$ codesign -s "Developer ID Application" \
  MyApp.app/Contents/MacOS/helper-tool

$ codesign -s "Developer ID Application" MyApp.app

# Or use --deep (less recommended, can miss components)
$ codesign -s "Developer ID Application" --deep MyApp.app

Signature Verification Script

#!/bin/bash
# verify-signature.sh - Comprehensive signature verification

APP="$1"

if [[ -z "$APP" ]]; then
    echo "Usage: $0 /path/to/app"
    exit 1
fi

echo "=== Signature Verification ==="
echo "Application: $APP"
echo

echo "--- Basic Verification ---"
codesign -v "$APP"
echo

echo "--- Signature Details ---"
codesign -dv "$APP" 2>&1
echo

echo "--- Entitlements ---"
codesign -d --entitlements - "$APP" 2>&1
echo

echo "--- Gatekeeper Assessment ---"
spctl --assess -v "$APP" 2>&1
echo

echo "--- Quarantine Status ---"
QUARANTINE=$(xattr -p com.apple.quarantine "$APP" 2>/dev/null)
if [[ -n "$QUARANTINE" ]]; then
    echo "Quarantined: $QUARANTINE"
else
    echo "Not quarantined"
fi

Security Audit Commands

# Find unsigned applications in /Applications
$ for app in /Applications/*.app; do
    codesign -v "$app" 2>/dev/null || echo "Unsigned: $app"
done

# Check signature validity of all running applications
$ for pid in $(pgrep -x '^[A-Z]'); do
    path=$(ps -p $pid -o comm= 2>/dev/null)
    if [[ -n "$path" ]]; then
        codesign -v "$path" 2>/dev/null || echo "Invalid: $path (PID: $pid)"
    fi
done

# List all quarantined files in Downloads
$ mdfind "kMDItemWhereFroms == '*'" -onlyin ~/Downloads 2>/dev/null | while read f; do
    xattr -p com.apple.quarantine "$f" 2>/dev/null && echo "$f"
done

# Check all kexts are properly signed
$ kextstat | awk 'NR>1 {print $6}' | while read bundle; do
    kextpath=$(find /System/Library/Extensions /Library/Extensions -name "$bundle.kext" 2>/dev/null | head -1)
    if [[ -n "$kextpath" ]]; then
        codesign -v "$kextpath" 2>/dev/null || echo "Issue with: $bundle"
    fi
done

Summary

Code signing and Gatekeeper form the first line of defense for macOS application security:

ToolPurpose
codesignSign and verify code signatures
spctlManage Gatekeeper policies
xattrView/modify quarantine attributes
security find-identityList signing certificates

Key commands:

# Verify signature
$ codesign -v /path/to/app

# Assess with Gatekeeper
$ spctl --assess -v /path/to/app

# View signature details
$ codesign -dv --verbose=4 /path/to/app

# View entitlements
$ codesign -d --entitlements - /path/to/app

# Remove quarantine
$ xattr -dr com.apple.quarantine /path/to/app

# Sign your own code
$ codesign -s "Developer ID Application" --options runtime /path/to/app

Code signing ensures that software comes from a known source and hasn’t been modified. Combined with notarization (covered in the next chapter), it provides a robust trust system for macOS software distribution.

Notarization Requirements

Notarization is Apple’s automated security scanning service for software distributed outside the Mac App Store. When you notarize your software, Apple scans it for malware and known security issues, then issues a “ticket” that Gatekeeper recognizes. Since macOS 10.15 Catalina, notarization is required for all Developer ID signed software to pass Gatekeeper without warnings.

What Notarization Does

┌─────────────────────────────────────────────────────────────────┐
│                    Notarization Process                          │
├─────────────────────────────────────────────────────────────────┤
│  1. Developer uploads signed app to Apple                        │
│                         ↓                                        │
│  2. Apple performs automated security scans:                     │
│     - Malware detection                                          │
│     - Hardened runtime verification                              │
│     - Signature validation                                       │
│     - Unsafe API usage detection                                 │
│                         ↓                                        │
│  3. Apple issues notarization ticket                             │
│                         ↓                                        │
│  4. Developer staples ticket to software (optional)              │
│                         ↓                                        │
│  5. User downloads software                                      │
│                         ↓                                        │
│  6. Gatekeeper verifies:                                         │
│     - Valid Developer ID signature                               │
│     - Notarization ticket (from staple or Apple servers)         │
└─────────────────────────────────────────────────────────────────┘

Notarization Requirements

To successfully notarize software, it must meet these requirements:

Code Signing Requirements

  1. Signed with Developer ID certificate (not Apple Development)
  2. Hardened runtime enabled (--options runtime)
  3. Secure timestamp included (--timestamp)
  4. All nested code must be signed (frameworks, helpers, plugins)

Technical Requirements

# The app must be signed with these minimum options:
$ codesign -s "Developer ID Application: Your Name (TEAMID)" \
  --timestamp \
  --options runtime \
  MyApp.app

# Verify hardened runtime is enabled
$ codesign -dv MyApp.app 2>&1 | grep flags
CodeDirectory v=20500 size=1234 flags=0x10000(runtime) hashes=42+7 location=embedded

Common Notarization Blockers

IssueSolution
No hardened runtimeAdd --options runtime to codesign
Missing timestampAdd --timestamp to codesign
Unsigned nested codeSign all frameworks and helpers
Invalid signatureRe-sign with valid Developer ID
Forbidden entitlementsRemove or get Apple approval
Insecure library loadingUse @rpath or hardcoded paths

The notarytool Command

Apple’s current notarization tool is notarytool, included with Xcode command-line tools. It replaced the older altool starting with Xcode 13.

Storing Credentials

Create an app-specific password at appleid.apple.com, then store credentials:

# Store credentials in keychain (recommended for scripts)
$ xcrun notarytool store-credentials "notarization-profile" \
  --apple-id "your@email.com" \
  --team-id "TEAMID123" \
  --password "app-specific-password"

# Credentials are stored in keychain
# Profile name can be anything meaningful to you

# Verify stored credentials
$ security find-generic-password -l "notarization-profile" -w 2>/dev/null && \
  echo "Credentials stored successfully"

Submitting for Notarization

Notarization works with zip archives, disk images, or installer packages:

# Create a zip archive of your app
$ ditto -c -k --keepParent MyApp.app MyApp.zip

# Submit for notarization using stored credentials
$ xcrun notarytool submit MyApp.zip \
  --keychain-profile "notarization-profile" \
  --wait

# Output shows submission progress:
# Conducting pre-submission checks for MyApp.zip and initiating connection to the Apple notary service...
# Submission ID received
#   id: 12345678-1234-1234-1234-123456789012
# Successfully uploaded file
# Waiting for processing to complete.
# ....
# Processing complete
#   id: 12345678-1234-1234-1234-123456789012
#   status: Accepted

# Submit without waiting (returns immediately)
$ xcrun notarytool submit MyApp.zip \
  --keychain-profile "notarization-profile"
# Save the submission ID to check status later

Checking Submission Status

# Check status of a submission
$ xcrun notarytool info 12345678-1234-1234-1234-123456789012 \
  --keychain-profile "notarization-profile"

# Get detailed log (useful when submission fails)
$ xcrun notarytool log 12345678-1234-1234-1234-123456789012 \
  --keychain-profile "notarization-profile"

# Save log to file for analysis
$ xcrun notarytool log 12345678-1234-1234-1234-123456789012 \
  --keychain-profile "notarization-profile" \
  developer_log.json

# View submission history
$ xcrun notarytool history --keychain-profile "notarization-profile"

Notarization Log Analysis

When notarization fails, the log provides details:

# Download and examine the log
$ xcrun notarytool log SUBMISSION_ID \
  --keychain-profile "notarization-profile" \
  log.json

# View formatted log
$ cat log.json | python3 -m json.tool

# Common issues in logs:
# - "The signature does not include a secure timestamp"
# - "The executable does not have the hardened runtime enabled"
# - "The binary is not signed with a valid Developer ID certificate"
# - "The signature of the binary is invalid"

Stapling

Stapling attaches the notarization ticket directly to your software, allowing offline verification:

# Staple ticket to an app
$ xcrun stapler staple MyApp.app
Processing: /path/to/MyApp.app
Processing: /path/to/MyApp.app
The staple and validate action worked!

# Staple to a disk image
$ xcrun stapler staple MyApp.dmg

# Staple to an installer package
$ xcrun stapler staple MyInstaller.pkg

# Verify stapling
$ xcrun stapler validate MyApp.app
Processing: /path/to/MyApp.app
The validate action worked!

# Check if an app has a stapled ticket
$ spctl -a -vvv MyApp.app 2>&1 | grep -i "ticket"

Why Stapling Matters

Without stapling:

  • Gatekeeper must contact Apple’s servers on first launch
  • Won’t work if user is offline
  • Slower first-launch experience

With stapling:

  • Offline verification possible
  • Faster first launch
  • Better user experience

Complete Notarization Workflow

Here’s a complete script for signing, notarizing, and stapling:

#!/bin/bash
# notarize.sh - Complete notarization workflow

set -e

APP_NAME="MyApp"
APP_PATH="./build/${APP_NAME}.app"
DEVELOPER_ID="Developer ID Application: Your Name (TEAMID)"
KEYCHAIN_PROFILE="notarization-profile"
BUNDLE_ID="com.example.myapp"

echo "=== Step 1: Sign the application ==="
# Sign all nested components first
find "$APP_PATH" -name "*.framework" -exec \
  codesign -s "$DEVELOPER_ID" --timestamp --options runtime {} \;

find "$APP_PATH" -name "*.dylib" -exec \
  codesign -s "$DEVELOPER_ID" --timestamp --options runtime {} \;

# Sign helper tools
if [[ -d "$APP_PATH/Contents/Library/LoginItems" ]]; then
  find "$APP_PATH/Contents/Library/LoginItems" -name "*.app" -exec \
    codesign -s "$DEVELOPER_ID" --timestamp --options runtime {} \;
fi

# Sign the main app
codesign -s "$DEVELOPER_ID" \
  --timestamp \
  --options runtime \
  --entitlements entitlements.plist \
  "$APP_PATH"

echo "=== Step 2: Verify signature ==="
codesign -vvv --deep --strict "$APP_PATH"

echo "=== Step 3: Create ZIP for notarization ==="
ZIP_PATH="./build/${APP_NAME}.zip"
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"

echo "=== Step 4: Submit for notarization ==="
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
  --keychain-profile "$KEYCHAIN_PROFILE" \
  --wait 2>&1)

echo "$SUBMIT_OUTPUT"

# Check if successful
if echo "$SUBMIT_OUTPUT" | grep -q "status: Accepted"; then
  echo "=== Step 5: Staple the ticket ==="
  xcrun stapler staple "$APP_PATH"

  echo "=== Step 6: Verify final product ==="
  spctl -a -vvv "$APP_PATH"

  echo "=== Notarization complete! ==="
else
  echo "=== Notarization failed ==="
  # Extract submission ID and get log
  SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
  if [[ -n "$SUBMISSION_ID" ]]; then
    echo "Fetching detailed log..."
    xcrun notarytool log "$SUBMISSION_ID" \
      --keychain-profile "$KEYCHAIN_PROFILE"
  fi
  exit 1
fi

Notarizing Disk Images

For distributing via DMG:

# Create the DMG
$ hdiutil create -volname "MyApp" \
  -srcfolder MyApp.app \
  -ov -format UDZO \
  MyApp.dmg

# Sign the DMG
$ codesign -s "Developer ID Application: Your Name (TEAMID)" \
  --timestamp \
  MyApp.dmg

# Submit for notarization
$ xcrun notarytool submit MyApp.dmg \
  --keychain-profile "notarization-profile" \
  --wait

# Staple the ticket
$ xcrun stapler staple MyApp.dmg

Notarizing Installer Packages

# Build the package with pkgbuild
$ pkgbuild --root ./payload \
  --identifier com.example.myapp \
  --version 1.0 \
  --install-location /Applications \
  MyApp-unsigned.pkg

# Sign with Developer ID Installer certificate
$ productsign --sign "Developer ID Installer: Your Name (TEAMID)" \
  MyApp-unsigned.pkg \
  MyApp.pkg

# Submit for notarization
$ xcrun notarytool submit MyApp.pkg \
  --keychain-profile "notarization-profile" \
  --wait

# Staple
$ xcrun stapler staple MyApp.pkg

Notarizing Command-Line Tools

Command-line tools can also be notarized:

# Sign the binary
$ codesign -s "Developer ID Application: Your Name (TEAMID)" \
  --timestamp \
  --options runtime \
  mytool

# Create a zip for submission
$ zip mytool.zip mytool

# Submit
$ xcrun notarytool submit mytool.zip \
  --keychain-profile "notarization-profile" \
  --wait

# Note: Can't staple directly to a binary
# Distribute as signed zip or embed in pkg/dmg

Checking Notarization Status

Verify that distributed software is properly notarized:

# Check if an app is notarized
$ spctl -a -vvv /Applications/SomeApp.app
/Applications/SomeApp.app: accepted
source=Notarized Developer ID
origin=Developer ID Application: Company Name (TEAMID)

# If not notarized, you'll see:
# source=Developer ID
# (without "Notarized" prefix)

# Check for stapled ticket
$ stapler validate /Applications/SomeApp.app
Processing: /Applications/SomeApp.app
The validate action worked!

# If no ticket stapled:
# The validate action worked!
# (but without the Processing line showing the ticket)

Troubleshooting Notarization

Common Issues and Solutions

“The signature does not include a secure timestamp”

# Always use --timestamp when signing
$ codesign -s "Developer ID Application" --timestamp MyApp.app

“The executable does not have the hardened runtime enabled”

# Enable hardened runtime
$ codesign -s "Developer ID Application" \
  --timestamp \
  --options runtime \
  MyApp.app

“The binary uses an SDK older than the 10.9 SDK”

# Rebuild with a newer SDK
# In Xcode, set Deployment Target to 10.9 or later

“Found an unsigned library”

# Find unsigned components
$ codesign -vvv --deep --strict MyApp.app 2>&1 | grep "not signed"

# Sign each component
$ codesign -s "Developer ID Application" --timestamp path/to/unsigned.dylib

Debugging Script

#!/bin/bash
# check-notarization-ready.sh - Verify app is ready for notarization

APP="$1"

if [[ -z "$APP" ]]; then
    echo "Usage: $0 /path/to/app"
    exit 1
fi

echo "=== Checking $APP for notarization readiness ==="

# Check signature exists
echo -n "Signature: "
if codesign -v "$APP" 2>/dev/null; then
    echo "Valid"
else
    echo "INVALID or MISSING"
    codesign -vvv "$APP"
    exit 1
fi

# Check hardened runtime
echo -n "Hardened Runtime: "
FLAGS=$(codesign -dv "$APP" 2>&1 | grep flags | grep -o "0x[0-9a-f]*")
if [[ "$FLAGS" == *"10000"* ]] || [[ "$FLAGS" == *"runtime"* ]]; then
    echo "Enabled"
else
    echo "DISABLED"
    echo "  Add --options runtime to codesign"
fi

# Check timestamp
echo -n "Secure Timestamp: "
if codesign -dv "$APP" 2>&1 | grep -q "Timestamp="; then
    echo "Present"
else
    echo "MISSING"
    echo "  Add --timestamp to codesign"
fi

# Check Developer ID
echo -n "Developer ID: "
AUTHORITY=$(codesign -dv "$APP" 2>&1 | grep "Authority=Developer ID")
if [[ -n "$AUTHORITY" ]]; then
    echo "Yes"
else
    echo "NO"
    echo "  Must be signed with Developer ID certificate"
fi

# Check for nested unsigned code
echo "Checking nested code..."
UNSIGNED=$(codesign -vvv --deep --strict "$APP" 2>&1 | grep -i "not signed")
if [[ -n "$UNSIGNED" ]]; then
    echo "WARNING: Unsigned components found:"
    echo "$UNSIGNED"
fi

# Test Gatekeeper assessment
echo -n "Gatekeeper Assessment: "
spctl -a -vvv "$APP" 2>&1 | head -2

echo "=== Check complete ==="

Entitlements for Notarization

Some entitlements require special handling for notarization:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- Required for JIT (like JavaScriptCore) -->
    <key>com.apple.security.cs.allow-jit</key>
    <true/>

    <!-- Allow unsigned executable memory (avoid if possible) -->
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>

    <!-- Disable library validation (for plugins) -->
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>

    <!-- Allow DYLD environment variables -->
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
</dict>
</plist>

Certain entitlements (like com.apple.security.cs.allow-unsigned-executable-memory) may cause notarization to fail or require additional review.

Summary

Notarization is mandatory for Developer ID signed software on modern macOS:

ComponentPurpose
notarytool submitUpload software for scanning
notarytool infoCheck submission status
notarytool logGet detailed scan results
stapler stapleAttach ticket to software
spctl -a -vvvVerify notarization

Key requirements:

  • Developer ID certificate (not Apple Development)
  • Hardened runtime enabled (--options runtime)
  • Secure timestamp (--timestamp)
  • All nested code signed
  • No blocked entitlements
# Quick notarization workflow
$ codesign -s "Developer ID Application" --timestamp --options runtime MyApp.app
$ ditto -c -k --keepParent MyApp.app MyApp.zip
$ xcrun notarytool submit MyApp.zip --keychain-profile "profile" --wait
$ xcrun stapler staple MyApp.app

Without notarization, your users will see scary Gatekeeper warnings or be unable to run your software at all. Proper notarization ensures a smooth, trusted experience.

App Sandboxing

Sandboxing is macOS’s containment mechanism that restricts what resources an application can access. A sandboxed app operates in a constrained environment with limited access to the file system, network, hardware, and system services. While App Store apps are required to be sandboxed, understanding sandboxing is valuable for security testing, developing distributed software, and restricting untrusted processes.

How Sandboxing Works

┌─────────────────────────────────────────────────────────────────┐
│                    macOS Kernel (XNU)                            │
│                          │                                       │
│              ┌──────────────────────────┐                       │
│              │   Sandbox Kernel Module   │                       │
│              │       (Seatbelt)          │                       │
│              └──────────────────────────┘                       │
│                          │                                       │
│         ┌────────────────┼────────────────┐                     │
│         ↓                ↓                ↓                     │
│  ┌────────────┐   ┌────────────┐   ┌────────────┐              │
│  │ Sandbox    │   │ Sandbox    │   │ Sandbox    │              │
│  │ Container  │   │ Container  │   │ Container  │              │
│  │            │   │            │   │            │              │
│  │  App A     │   │  App B     │   │  App C     │              │
│  │            │   │            │   │            │              │
│  │ ~/Library/ │   │ ~/Library/ │   │ ~/Library/ │              │
│  │ Containers/│   │ Containers/│   │ Containers/│              │
│  │ com.a.app/ │   │ com.b.app/ │   │ com.c.app/ │              │
│  └────────────┘   └────────────┘   └────────────┘              │
└─────────────────────────────────────────────────────────────────┘

Each sandboxed app gets its own container directory and can only access:

  • Its container directory
  • Resources explicitly granted via entitlements
  • User-selected files (via Open/Save dialogs)
  • Resources granted through security-scoped bookmarks

Sandbox Containers

Sandboxed apps store their data in containers:

# List sandbox containers
$ ls ~/Library/Containers/
com.apple.Notes
com.apple.Safari
com.example.myapp

# View a container's structure
$ ls ~/Library/Containers/com.apple.Safari/
Data

$ ls ~/Library/Containers/com.apple.Safari/Data/
Documents
Library

# Each container has standard directories
$ ls ~/Library/Containers/com.apple.Safari/Data/Library/
Application Support
Caches
HTTPStorages
Preferences
Saved Application State

# Container metadata
$ ls -la ~/Library/Containers/com.apple.Safari/
total 0
drwx------  3 david  staff   96 Jan 15 10:00 .
drwx------  5 david  staff  160 Jan 15 10:00 ..
drwx------  5 david  staff  160 Jan 15 10:00 Data
lrwxr-xr-x  1 david  staff   72 Jan 15 10:00 .com.apple.containermanagerd.metadata.plist -> ...

Group Containers

Apps can share data through group containers:

# Group containers
$ ls ~/Library/Group\ Containers/
group.com.apple.notes
group.com.example.shared

# Apps with the same app group entitlement can access shared containers

The sandbox-exec Command

The sandbox-exec command runs processes with sandbox restrictions. While primarily used for testing, it demonstrates sandbox capabilities:

Basic Usage

# Run a command with no network access
$ sandbox-exec -n no-network curl https://example.com
curl: (6) Could not resolve host: example.com

# Run with no file write access
$ sandbox-exec -n no-write touch /tmp/test
touch: /tmp/test: Operation not permitted

# List built-in profiles (may vary by macOS version)
$ ls /System/Library/Sandbox/Profiles/
# Note: Many profiles are embedded and not visible as files

Built-in Sandbox Profiles

macOS includes several built-in profiles:

ProfileDescription
no-internetBlocks all network access
no-networkBlocks network access
no-writeBlocks file writes
no-write-except-temporaryAllows writes only to temp
pure-computationMinimal access for computation only
# Examples using built-in profiles
$ sandbox-exec -n no-internet python3 -c "import urllib.request; print(urllib.request.urlopen('https://example.com').read())"
# Fails with network error

$ sandbox-exec -n no-write-except-temporary bash -c 'echo test > /tmp/ok.txt && echo test > ~/fail.txt'
# First write succeeds, second fails

Custom Sandbox Profiles

Create custom profiles using Scheme-like syntax (SBPL - Sandbox Profile Language):

;; my-sandbox.sb - Custom sandbox profile
(version 1)

;; Start with deny-all
(deny default)

;; Allow reading most files
(allow file-read*)

;; Allow writing to specific directories
(allow file-write*
    (subpath "/tmp")
    (subpath (param "TMPDIR")))

;; Allow specific network access
(allow network-outbound
    (remote tcp "example.com:443"))

;; Allow process execution
(allow process-exec)
(allow process-fork)

;; Allow reading system libraries
(allow file-read-data
    (subpath "/usr/lib")
    (subpath "/System/Library"))

;; Allow mach services for basic functionality
(allow mach-lookup
    (global-name "com.apple.system.logger"))

Use the custom profile:

$ sandbox-exec -f my-sandbox.sb /path/to/command

Profile Variables

Pass variables to sandbox profiles:

# Profile using parameter
# (allow file-write* (subpath (param "WRITEPATH")))

$ sandbox-exec -f my-profile.sb -D WRITEPATH=/tmp/myapp /path/to/command

Debugging Sandbox Profiles

# Enable sandbox tracing (verbose)
$ sandbox-exec -f my-profile.sb -D _TRACE=1 /path/to/command

# View sandbox violations in system log
$ log show --predicate 'subsystem == "com.apple.sandbox"' --last 5m

# Stream sandbox violations
$ log stream --predicate 'subsystem == "com.apple.sandbox"'

App Sandbox Entitlements

Apps declare their sandbox capabilities through entitlements:

Common Sandbox Entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- Enable App Sandbox -->
    <key>com.apple.security.app-sandbox</key>
    <true/>

    <!-- File Access -->
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>

    <key>com.apple.security.files.downloads.read-write</key>
    <true/>

    <!-- Network Access -->
    <key>com.apple.security.network.client</key>
    <true/>

    <key>com.apple.security.network.server</key>
    <true/>

    <!-- Hardware Access -->
    <key>com.apple.security.device.camera</key>
    <true/>

    <key>com.apple.security.device.microphone</key>
    <true/>

    <key>com.apple.security.device.usb</key>
    <true/>

    <!-- Personal Information -->
    <key>com.apple.security.personal-information.addressbook</key>
    <true/>

    <key>com.apple.security.personal-information.calendars</key>
    <true/>

    <key>com.apple.security.personal-information.location</key>
    <true/>

    <!-- Temporary Exception (for legacy code) -->
    <key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
    <array>
        <string>/usr/local/</string>
    </array>
</dict>
</plist>

Viewing App Entitlements

# View entitlements of a running app
$ codesign -d --entitlements - /Applications/Safari.app

# Check if an app is sandboxed
$ codesign -d --entitlements - /Applications/Notes.app 2>&1 | grep "app-sandbox"
[Key] com.apple.security.app-sandbox
[Value]
    [Bool] true

# View entitlements as XML
$ codesign -d --entitlements :- /Applications/Safari.app

File Access Entitlements

EntitlementAccess Granted
files.user-selected.read-onlyRead files user opens
files.user-selected.read-writeRead/write user-opened files
files.downloads.read-onlyRead ~/Downloads
files.downloads.read-writeRead/write ~/Downloads
files.pictures.read-onlyRead ~/Pictures
files.pictures.read-writeRead/write ~/Pictures
files.music.read-onlyRead ~/Music
files.music.read-writeRead/write ~/Music
files.movies.read-onlyRead ~/Movies
files.movies.read-writeRead/write ~/Movies

Network Entitlements

<!-- Outgoing connections -->
<key>com.apple.security.network.client</key>
<true/>

<!-- Incoming connections (server) -->
<key>com.apple.security.network.server</key>
<true/>

Security-Scoped Bookmarks

Sandboxed apps can maintain access to user-selected files across launches using security-scoped bookmarks:

# Security-scoped bookmarks are created programmatically
# They allow persistent access to files outside the container

# View bookmark data (binary format)
$ xxd -l 100 ~/Library/Containers/com.example.app/Data/Library/Application\ Support/bookmarks.data

This is primarily an API feature, but understanding it helps debug sandbox behavior.

Checking Sandbox Status

For Running Processes

# Check if a process is sandboxed
$ ps -p <PID> -o flags
# Look for sandbox flag

# More detailed check using asctl (App Sandbox status)
$ asctl sandbox check --pid <PID>

# List sandboxed processes
$ ps aux | while read line; do
    pid=$(echo "$line" | awk '{print $2}')
    if sandbox-check 2>/dev/null; then
        echo "$line"
    fi
done

For Applications

# Check if an app bundle is sandboxed
$ codesign -d --entitlements - /path/to/app.app 2>&1 | grep -A1 "app-sandbox"

# Quick script to check sandbox status
#!/bin/bash
check_sandbox() {
    local app="$1"
    if codesign -d --entitlements - "$app" 2>&1 | grep -q "com.apple.security.app-sandbox"; then
        echo "Sandboxed: $app"
    else
        echo "Not sandboxed: $app"
    fi
}

check_sandbox /Applications/Safari.app
check_sandbox /Applications/iTerm.app

Sandbox Violations

When a sandboxed app tries to do something it’s not allowed to, a violation occurs:

Viewing Violations

# View recent sandbox violations
$ log show --predicate 'subsystem == "com.apple.sandbox" AND category == "violation"' --last 1h

# Stream violations in real-time
$ log stream --predicate 'subsystem == "com.apple.sandbox" AND category == "violation"'

# Example violation log:
# Sandbox: MyApp(1234) deny(1) file-read-data /private/etc/passwd

Common Violation Patterns

# File access violation
# Sandbox: app(pid) deny file-read-data /path/to/file

# Network violation
# Sandbox: app(pid) deny network-outbound

# Mach service violation
# Sandbox: app(pid) deny mach-lookup com.apple.some.service

# Hardware access violation
# Sandbox: app(pid) deny device-camera

Testing Sandbox Restrictions

Create test environments to verify sandbox behavior:

#!/bin/bash
# test-sandbox.sh - Test sandbox restrictions

# Test no-write sandbox
echo "Testing no-write sandbox..."
sandbox-exec -n no-write bash -c 'echo test > /tmp/test.txt' 2>&1 && echo "FAIL" || echo "PASS: Write blocked"

# Test no-network sandbox
echo "Testing no-network sandbox..."
sandbox-exec -n no-network curl -s https://example.com 2>&1 | grep -q "Could not resolve" && echo "PASS: Network blocked" || echo "FAIL"

# Test custom profile
cat > /tmp/test-sandbox.sb << 'EOF'
(version 1)
(deny default)
(allow file-read* (subpath "/"))
(allow file-write* (subpath "/tmp"))
(deny file-write* (subpath "/tmp/restricted"))
(allow process-exec)
(allow process-fork)
EOF

mkdir -p /tmp/restricted

echo "Testing custom sandbox..."
sandbox-exec -f /tmp/test-sandbox.sb bash -c 'echo test > /tmp/ok.txt' 2>&1 && echo "PASS: /tmp write allowed" || echo "FAIL"
sandbox-exec -f /tmp/test-sandbox.sb bash -c 'echo test > /tmp/restricted/fail.txt' 2>&1 && echo "FAIL" || echo "PASS: /tmp/restricted blocked"

rm /tmp/test-sandbox.sb /tmp/ok.txt 2>/dev/null
rmdir /tmp/restricted 2>/dev/null

Container Management

Viewing Container Contents

# List all containers
$ ls ~/Library/Containers/

# Find container for a specific app
$ ls -la ~/Library/Containers/ | grep Safari

# View container size
$ du -sh ~/Library/Containers/com.apple.Safari/

# Find containers using significant space
$ du -sh ~/Library/Containers/* 2>/dev/null | sort -hr | head -10

Resetting Containers

# Remove an app's container (resets app to fresh state)
# WARNING: Deletes all app data
$ rm -rf ~/Library/Containers/com.example.app/

# The container is recreated when the app launches

Sandboxed apps see a different filesystem view:

# Inside a sandboxed app, ~ points to the container
# ~/Library is actually ~/Library/Containers/com.example.app/Data/Library

# Some directories are symlinked to real locations
$ ls -la ~/Library/Containers/com.apple.Safari/Data/
# Desktop -> /Users/david/Desktop (if allowed by entitlements)

Sandbox Profiles for System Services

Many macOS system services run sandboxed:

# View sandbox profiles in use
$ sudo find /System -name "*.sb" -type f 2>/dev/null

# System sandbox profiles location
$ ls /System/Library/Sandbox/Profiles/ 2>/dev/null

# Many profiles are now embedded in the Sandbox kernel extension
# and not visible as separate files

Practical Examples

Restricting a Build Process

# Run a build with restricted file access
$ cat > build-sandbox.sb << 'EOF'
(version 1)
(deny default)
(allow file-read*)
(allow file-write*
    (subpath "/tmp")
    (subpath (param "BUILD_DIR")))
(allow process-exec)
(allow process-fork)
(allow mach-lookup)
EOF

$ sandbox-exec -f build-sandbox.sb -D BUILD_DIR="$(pwd)/build" make

Running Untrusted Scripts

# Run an untrusted script with minimal permissions
$ cat > untrusted-sandbox.sb << 'EOF'
(version 1)
(deny default)
(allow file-read*
    (subpath "/usr/lib")
    (subpath "/usr/bin")
    (subpath "/bin")
    (subpath "/System/Library"))
(allow file-read-data (literal "/dev/null"))
(allow file-write-data (literal "/dev/null"))
(allow process-exec)
(allow process-fork)
EOF

$ sandbox-exec -f untrusted-sandbox.sb bash untrusted-script.sh

Summary

Sandboxing provides containment for macOS applications:

ComponentPurpose
App SandboxIsolates Mac App Store apps
ContainersPer-app data directories
EntitlementsDeclare required capabilities
sandbox-execRun commands with restrictions
Sandbox profilesDefine access rules

Key concepts:

  • Sandboxed apps have limited filesystem access
  • Access must be declared via entitlements
  • User can grant additional access via file dialogs
  • Violations are logged and can be monitored
  • System services also use sandboxing
# Check if app is sandboxed
$ codesign -d --entitlements - /path/to/app | grep app-sandbox

# Run command with restrictions
$ sandbox-exec -n no-network /path/to/command

# View sandbox violations
$ log show --predicate 'subsystem == "com.apple.sandbox"' --last 10m

Sandboxing is a critical defense-in-depth layer that limits the damage a compromised application can do, even if other security measures fail.

Keychain Services from Terminal

The macOS Keychain is a secure, encrypted database that stores passwords, certificates, encryption keys, and secure notes. While most users interact with Keychain through the Keychain Access GUI application, the security command-line tool provides full access to Keychain services for automation, scripting, and administrative tasks.

Understanding Keychains

macOS maintains several keychains by default:

┌─────────────────────────────────────────────────────────────────┐
│                    Keychain Hierarchy                            │
├─────────────────────────────────────────────────────────────────┤
│  System Keychain                                                 │
│  /Library/Keychains/System.keychain                             │
│  - System-wide certificates and passwords                        │
│  - Requires admin authentication                                 │
├─────────────────────────────────────────────────────────────────┤
│  System Roots                                                    │
│  /System/Library/Keychains/SystemRootCertificates.keychain      │
│  - Apple's trusted root certificates                             │
│  - Protected by SIP                                              │
├─────────────────────────────────────────────────────────────────┤
│  Login Keychain                                                  │
│  ~/Library/Keychains/login.keychain-db                          │
│  - User's personal passwords and certificates                    │
│  - Unlocked at login by default                                  │
├─────────────────────────────────────────────────────────────────┤
│  Local Items (iCloud Keychain)                                   │
│  ~/Library/Keychains/[UUID]/                                    │
│  - Synced across devices via iCloud                              │
│  - Protected by Secure Enclave                                   │
└─────────────────────────────────────────────────────────────────┘

The security Command

Keychain Management

# List all keychains in search list
$ security list-keychains
    "/Users/david/Library/Keychains/login.keychain-db"
    "/Library/Keychains/System.keychain"

# Show default keychain
$ security default-keychain
    "/Users/david/Library/Keychains/login.keychain-db"

# Set default keychain
$ security default-keychain -s ~/Library/Keychains/login.keychain-db

# Show login keychain
$ security login-keychain
    "/Users/david/Library/Keychains/login.keychain-db"

# Get keychain info
$ security show-keychain-info ~/Library/Keychains/login.keychain-db
Keychain "/Users/david/Library/Keychains/login.keychain-db"
    lock-on-sleep
    timeout=21600s

Creating and Deleting Keychains

# Create a new keychain
$ security create-keychain -p "password" ~/Library/Keychains/custom.keychain

# Create with password prompt (more secure)
$ security create-keychain ~/Library/Keychains/custom.keychain
password for new keychain:

# Add to search list
$ security list-keychains -s ~/Library/Keychains/login.keychain-db \
    ~/Library/Keychains/custom.keychain

# Delete a keychain
$ security delete-keychain ~/Library/Keychains/custom.keychain

Locking and Unlocking

# Lock a keychain
$ security lock-keychain ~/Library/Keychains/login.keychain-db

# Unlock a keychain (prompts for password)
$ security unlock-keychain ~/Library/Keychains/login.keychain-db

# Unlock with password (for scripts - be careful with password exposure)
$ security unlock-keychain -p "password" ~/Library/Keychains/login.keychain-db

# Lock all keychains
$ security lock-keychain -a

# Set keychain to lock after timeout
$ security set-keychain-settings -t 3600 ~/Library/Keychains/login.keychain-db
# Locks after 1 hour of inactivity

# Set to lock on sleep
$ security set-keychain-settings -l ~/Library/Keychains/login.keychain-db

Managing Passwords

Finding Passwords

# Find a generic password
$ security find-generic-password -a "username" -s "service" \
    ~/Library/Keychains/login.keychain-db

# Output includes metadata:
# keychain: "/Users/david/Library/Keychains/login.keychain-db"
# class: "genp"
# attributes:
#     "acct"<blob>="username"
#     "svce"<blob>="service"
#     ...

# Get just the password (to stdout)
$ security find-generic-password -a "username" -s "service" -w
mypassword

# Find internet password (website credentials)
$ security find-internet-password -a "username" -s "example.com" -w

# Find by label
$ security find-generic-password -l "My Label" -w

# Find with domain and protocol
$ security find-internet-password -s "github.com" -r "htps" -w

Adding Passwords

# Add a generic password
$ security add-generic-password \
    -a "username" \
    -s "service-name" \
    -w "password" \
    -l "Human Readable Label" \
    ~/Library/Keychains/login.keychain-db

# Add without specifying password (prompts)
$ security add-generic-password \
    -a "username" \
    -s "service-name" \
    -l "My Service" \
    -T "" \
    ~/Library/Keychains/login.keychain-db

# Add an internet password
$ security add-internet-password \
    -a "username" \
    -s "example.com" \
    -w "password" \
    -r "htps" \
    -l "Example Website" \
    ~/Library/Keychains/login.keychain-db

# Update existing password (use -U flag)
$ security add-generic-password \
    -a "username" \
    -s "service-name" \
    -w "new-password" \
    -U \
    ~/Library/Keychains/login.keychain-db

Password Options

# -a : Account name
# -s : Service name
# -w : Password (or -w to read from stdin)
# -l : Label
# -c : Creator (4-char code)
# -C : Type (4-char code)
# -D : Kind description
# -r : Protocol (htps, http, etc.)
# -p : Path
# -P : Port
# -j : Comment
# -T : Application path for ACL (use "" for no apps)
# -A : Allow all applications to access
# -U : Update existing item

Deleting Passwords

# Delete a generic password
$ security delete-generic-password -a "username" -s "service-name" \
    ~/Library/Keychains/login.keychain-db

# Delete an internet password
$ security delete-internet-password -a "username" -s "example.com" \
    ~/Library/Keychains/login.keychain-db

# Delete by label
$ security delete-generic-password -l "My Label" \
    ~/Library/Keychains/login.keychain-db

Managing Certificates

Viewing Certificates

# List all certificates in a keychain
$ security find-certificate -a ~/Library/Keychains/login.keychain-db

# List with details
$ security find-certificate -a -p ~/Library/Keychains/login.keychain-db
# Outputs in PEM format

# Find certificate by name
$ security find-certificate -c "Developer ID" -a

# Find certificate by email
$ security find-certificate -e "your@email.com" -a

# Show certificate details
$ security find-certificate -c "Developer ID" -p | openssl x509 -noout -text

# Count certificates
$ security find-certificate -a | grep -c "keychain:"

Adding Certificates

# Add a certificate to keychain
$ security add-certificates ~/path/to/certificate.cer

# Add to specific keychain
$ security add-certificates -k ~/Library/Keychains/login.keychain-db \
    ~/path/to/certificate.cer

# Add as trusted root (system keychain)
$ sudo security add-trusted-cert -d -r trustRoot \
    -k /Library/Keychains/System.keychain \
    ~/path/to/ca-certificate.cer

Managing Trust Settings

# Get trust settings for a certificate
$ security dump-trust-settings -d
# -d : Admin domain
# -s : System domain (read-only)

# Add trust setting
$ sudo security add-trusted-cert -d -r trustRoot \
    -p ssl -p basic \
    -k /Library/Keychains/System.keychain \
    ~/path/to/certificate.cer

# Remove trust setting
$ sudo security remove-trusted-cert -d ~/path/to/certificate.cer

# Verify certificate trust
$ security verify-cert -c ~/path/to/certificate.cer

Exporting Certificates

# Export certificate to PEM
$ security find-certificate -c "Certificate Name" -p > cert.pem

# Export certificate and key as PKCS12
$ security export -k ~/Library/Keychains/login.keychain-db \
    -t identities \
    -f pkcs12 \
    -o identity.p12

# Export specific identity
$ security find-identity -v -p codesigning
# Note the SHA-1 hash
$ security export -k ~/Library/Keychains/login.keychain-db \
    -t identities \
    -f pkcs12 \
    -o identity.p12 \
    -P "export-password"

Importing Certificates and Keys

# Import PKCS12 file
$ security import ~/path/to/identity.p12 \
    -k ~/Library/Keychains/login.keychain-db \
    -f pkcs12 \
    -P "password"

# Import PEM certificate
$ security import ~/path/to/certificate.pem \
    -k ~/Library/Keychains/login.keychain-db \
    -f pem

# Import with application access
$ security import ~/path/to/identity.p12 \
    -k ~/Library/Keychains/login.keychain-db \
    -f pkcs12 \
    -P "password" \
    -A  # Allow all applications

Managing Identities (Certificate + Private Key)

# List all identities (cert + key pairs)
$ security find-identity -v
  1) ABC123... "Developer ID Application: Your Name (TEAM)"
  2) DEF456... "Apple Development: your@email.com (TEAM)"
     2 valid identities found

# List code signing identities
$ security find-identity -v -p codesigning

# List SSL identities
$ security find-identity -v -p ssl

# Available policies:
# basic, ssl, smime, eap, ipsec, ichat, codesigning, sysdefault, timestamping

Managing Keys

# Generate a symmetric key
$ security generate-key -a AES -b 256 -l "My Encryption Key"

# List keys
$ security dump-keychain ~/Library/Keychains/login.keychain-db | grep -A5 "class: key"

# Import a key
$ security import ~/path/to/private.key \
    -k ~/Library/Keychains/login.keychain-db \
    -f pem \
    -t priv

# Export key
$ security export -k ~/Library/Keychains/login.keychain-db \
    -t privKeys \
    -f pem \
    -o private.pem

Access Control Lists (ACLs)

Control which applications can access keychain items:

# View ACL for a keychain item
$ security dump-keychain -a ~/Library/Keychains/login.keychain-db | grep -A20 "access:"

# Add password with specific app access
$ security add-generic-password \
    -a "username" \
    -s "service" \
    -w "password" \
    -T /Applications/Safari.app/Contents/MacOS/Safari \
    ~/Library/Keychains/login.keychain-db

# Add with no app access (requires manual approval each time)
$ security add-generic-password \
    -a "username" \
    -s "service" \
    -w "password" \
    -T "" \
    ~/Library/Keychains/login.keychain-db

# Allow all applications (less secure)
$ security add-generic-password \
    -a "username" \
    -s "service" \
    -w "password" \
    -A \
    ~/Library/Keychains/login.keychain-db

# Set ACL partition list (for codesigning)
$ security set-key-partition-list -S apple-tool:,apple:,codesign: \
    -s -k "keychain-password" \
    ~/Library/Keychains/login.keychain-db

Practical Scripts

Store and Retrieve Secrets in Scripts

#!/bin/bash
# secret-manager.sh - Manage secrets via keychain

SERVICE="com.example.myapp"
KEYCHAIN="$HOME/Library/Keychains/login.keychain-db"

store_secret() {
    local name="$1"
    local value="$2"

    security add-generic-password \
        -a "$name" \
        -s "$SERVICE" \
        -w "$value" \
        -U \
        "$KEYCHAIN" 2>/dev/null

    if [[ $? -eq 0 ]]; then
        echo "Secret '$name' stored successfully"
    else
        echo "Failed to store secret"
        return 1
    fi
}

get_secret() {
    local name="$1"

    security find-generic-password \
        -a "$name" \
        -s "$SERVICE" \
        -w "$KEYCHAIN" 2>/dev/null
}

delete_secret() {
    local name="$1"

    security delete-generic-password \
        -a "$name" \
        -s "$SERVICE" \
        "$KEYCHAIN" 2>/dev/null

    if [[ $? -eq 0 ]]; then
        echo "Secret '$name' deleted"
    else
        echo "Secret not found"
        return 1
    fi
}

# Usage
case "$1" in
    set)
        store_secret "$2" "$3"
        ;;
    get)
        get_secret "$2"
        ;;
    delete)
        delete_secret "$2"
        ;;
    *)
        echo "Usage: $0 {set|get|delete} name [value]"
        exit 1
        ;;
esac

CI/CD Keychain Setup

#!/bin/bash
# ci-keychain-setup.sh - Setup keychain for CI/CD builds

set -e

KEYCHAIN_NAME="build.keychain"
KEYCHAIN_PATH="$HOME/Library/Keychains/$KEYCHAIN_NAME"
KEYCHAIN_PASSWORD="${CI_KEYCHAIN_PASSWORD:-build}"

# Create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

# Set keychain settings (don't lock automatically)
security set-keychain-settings -t 3600 -u "$KEYCHAIN_PATH"

# Add to search list
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')

# Unlock keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

# Import certificate (base64 encoded in environment variable)
echo "$SIGNING_CERTIFICATE_P12_BASE64" | base64 --decode > /tmp/cert.p12
security import /tmp/cert.p12 \
    -k "$KEYCHAIN_PATH" \
    -P "$SIGNING_CERTIFICATE_PASSWORD" \
    -A \
    -T /usr/bin/codesign \
    -T /usr/bin/security

rm /tmp/cert.p12

# Allow codesign to access key without prompt
security set-key-partition-list -S apple-tool:,apple:,codesign: \
    -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

echo "CI keychain setup complete"

Backup Keychain Items

#!/bin/bash
# backup-keychain.sh - Export keychain items (certificates only)

BACKUP_DIR="$HOME/keychain-backup-$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

# Export certificates
security find-certificate -a -p > "$BACKUP_DIR/all-certificates.pem"

# Export identities (will prompt for keychain password)
security export \
    -k ~/Library/Keychains/login.keychain-db \
    -t identities \
    -f pkcs12 \
    -o "$BACKUP_DIR/identities.p12"

echo "Backup saved to $BACKUP_DIR"
echo "WARNING: Secure this backup - it contains sensitive cryptographic material"

Certificate Expiration Check

#!/bin/bash
# check-cert-expiry.sh - Check certificate expiration dates

echo "=== Certificate Expiration Report ==="
echo

security find-certificate -a -p 2>/dev/null | \
awk '/-----BEGIN CERTIFICATE-----/{cert=""} {cert=cert$0"\n"} /-----END CERTIFICATE-----/{print cert}' | \
while read -r cert_block; do
    if [[ -n "$cert_block" ]]; then
        subject=$(echo "$cert_block" | openssl x509 -noout -subject 2>/dev/null | sed 's/subject=//')
        expiry=$(echo "$cert_block" | openssl x509 -noout -enddate 2>/dev/null | sed 's/notAfter=//')

        if [[ -n "$expiry" ]]; then
            expiry_epoch=$(date -j -f "%b %d %H:%M:%S %Y %Z" "$expiry" "+%s" 2>/dev/null)
            now_epoch=$(date "+%s")
            days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

            if [[ $days_left -lt 30 ]]; then
                status="WARNING"
            else
                status="OK"
            fi

            printf "%-10s %-80s %s (%d days)\n" "[$status]" "${subject:0:80}" "$expiry" "$days_left"
        fi
    fi
done

Security Considerations

Keychain Security Best Practices

  1. Lock keychain when not in use

    $ security lock-keychain
    
  2. Set auto-lock timeout

    $ security set-keychain-settings -t 300 ~/Library/Keychains/login.keychain-db
    
  3. Use specific application ACLs

    $ security add-generic-password -T /path/to/specific/app -a user -s service -w pass
    
  4. Never put passwords directly in scripts

    # Bad
    PASSWORD="hardcoded"
    
    # Good
    PASSWORD=$(security find-generic-password -s "myservice" -w)
    
  5. Create separate keychains for automation

    $ security create-keychain -p "$SECURE_PASSWORD" ~/Library/Keychains/automation.keychain
    

Summary

The security command provides complete Keychain access from the terminal:

TaskCommand
List keychainssecurity list-keychains
Unlock keychainsecurity unlock-keychain
Find passwordsecurity find-generic-password -s "service" -w
Add passwordsecurity add-generic-password -s "service" -a "user" -w "pass"
List certificatessecurity find-certificate -a
Import certificatesecurity import cert.p12 -k keychain
List identitiessecurity find-identity -v -p codesigning

Key concepts:

  • Login keychain stores user credentials
  • System keychain stores system-wide items
  • Passwords can be stored/retrieved without GUI
  • ACLs control which apps can access items
  • Always use keychain for secrets in scripts
# Quick reference
$ security list-keychains                                    # List keychains
$ security find-generic-password -s "service" -w             # Get password
$ security add-generic-password -s "service" -a "user" -w "pass"  # Store password
$ security find-identity -v -p codesigning                   # List signing identities
$ security unlock-keychain                                   # Unlock keychain

The Keychain provides secure credential storage that integrates with macOS authentication, making it the proper place to store secrets rather than configuration files or environment variables.

FileVault and Disk Encryption

FileVault is macOS’s full-disk encryption technology that protects all data on your startup disk. Using XTS-AES-128 encryption with a 256-bit key, FileVault ensures that your data remains unreadable without proper authentication, even if someone physically removes your drive or steals your Mac. For anyone handling sensitive data, FileVault is essential.

How FileVault Works

┌─────────────────────────────────────────────────────────────────┐
│                    FileVault Architecture                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   User Password ──┐                                             │
│                   ├── Unlocks ──▶ Volume Master Key             │
│   Recovery Key ───┘                      │                      │
│                                          │                      │
│   iCloud Recovery ─── Optionally ──▶ Recovery via Apple ID     │
│                                          │                      │
│                                          ▼                      │
│                           ┌──────────────────────────┐          │
│                           │   XTS-AES-128 Encrypted  │          │
│                           │      APFS Volume         │          │
│                           │                          │          │
│                           │   macOS System + Data    │          │
│                           │                          │          │
│                           └──────────────────────────┘          │
│                                                                  │
│   Hardware Keys ───▶ Secure Enclave (T2/Apple Silicon)          │
│   (Additional layer on supported hardware)                       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

FileVault 2 vs Original FileVault

FileVault 2 (introduced in OS X Lion):

  • Encrypts entire startup disk
  • Uses XTS-AES-128 encryption
  • On-the-fly encryption/decryption
  • Recovery key support
  • Institutional key support for enterprises

Original FileVault (deprecated):

  • Only encrypted home folders
  • Used AES-128 in CBC mode
  • Created encrypted disk images

The fdesetup Command

The fdesetup command is the primary tool for managing FileVault from the terminal.

Checking FileVault Status

# Check if FileVault is enabled
$ fdesetup status
FileVault is On.

# Or if disabled
$ fdesetup status
FileVault is Off.

# Check encryption progress (during initial encryption)
$ fdesetup status
FileVault is On.
Encryption in progress: Percent complete = 45.00

# Detailed status with verbose output
$ sudo fdesetup status -v
FileVault is On.
Volume /dev/disk1s1 is encrypted

Alternative Status Checks

# Using diskutil
$ diskutil apfs list | grep -A10 "FileVault"

# Check APFS volume encryption status
$ diskutil info disk1s1 | grep "FileVault"
FileVault:                 Yes

# Detailed APFS encryption info
$ diskutil apfs listCryptoUsers disk1s1
Cryptographic users for disk1s1 (3 found)
|
+-- 12345678-1234-1234-1234-123456789012
|   Type: Local Open Directory User
|   User: david
|
+-- ABCDEF01-2345-6789-ABCD-EF0123456789
|   Type: Personal Recovery User
|
+-- FEDCBA98-7654-3210-FEDC-BA9876543210
|   Type: Institutional Recovery User

Enabling FileVault

Enable with Recovery Key

# Enable FileVault (prompts for credentials)
$ sudo fdesetup enable

# Enable and output recovery key
$ sudo fdesetup enable -outputplist

# Example output:
# <?xml version="1.0" encoding="UTF-8"?>
# <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
# <plist version="1.0">
# <dict>
#     <key>RecoveryKey</key>
#     <string>XXXX-XXXX-XXXX-XXXX-XXXX-XXXX</string>
# </dict>
# </plist>

# Save recovery key to file
$ sudo fdesetup enable -outputplist > ~/Desktop/recovery-key.plist

Enable with User List

# Create input plist specifying users
$ cat > /tmp/fv-enable.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Username</key>
    <string>david</string>
    <key>Password</key>
    <string>user-password</string>
</dict>
</plist>
EOF

# Enable with input plist
$ sudo fdesetup enable -inputplist < /tmp/fv-enable.plist

# Clean up (don't leave password in file)
$ rm /tmp/fv-enable.plist

Enable with Institutional Recovery Key

For enterprise environments:

# Create certificate for institutional recovery
$ sudo fdesetup enable \
    -certificate /path/to/FileVaultMaster.cer \
    -outputplist

# The institutional key allows IT to recover encrypted Macs
# without individual user passwords

Deferred Enablement

Enable FileVault at next login instead of immediately:

# Enable deferred enablement
$ sudo fdesetup enable -defer /path/to/recovery-key.plist

# Check deferred status
$ sudo fdesetup status
Deferred enablement appears to be active for user "david".

# The user will be prompted to enable FileVault at next login
# Recovery key will be written to specified plist

Disabling FileVault

# Disable FileVault (starts decryption)
$ sudo fdesetup disable

# Monitor decryption progress
$ fdesetup status
FileVault is Off.
Decryption in progress: Percent complete = 35.00

# Decryption continues in background
# Mac remains usable during decryption

Warning: Disabling FileVault leaves data unencrypted. Only disable when necessary.

Managing FileVault Users

FileVault-enabled users can unlock the disk at boot:

List Enabled Users

# List FileVault-enabled users
$ sudo fdesetup list
david,12345678-1234-1234-1234-123456789012
admin,ABCDEF01-2345-6789-ABCD-EF0123456789

# More detailed listing
$ diskutil apfs listCryptoUsers disk1s1

# Check if specific user is enabled
$ sudo fdesetup isactive -user david
true

Add FileVault User

# Add user to FileVault (prompts for passwords)
$ sudo fdesetup add -usertoadd newuser

# Add with input plist
$ cat > /tmp/fv-add.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Username</key>
    <string>admin</string>
    <key>Password</key>
    <string>admin-password</string>
    <key>AdditionalUsers</key>
    <array>
        <dict>
            <key>Username</key>
            <string>newuser</string>
            <key>Password</key>
            <string>newuser-password</string>
        </dict>
    </array>
</dict>
</plist>
EOF

$ sudo fdesetup add -inputplist < /tmp/fv-add.plist
$ rm /tmp/fv-add.plist

Remove FileVault User

# Remove user from FileVault
$ sudo fdesetup remove -user username

# The user's account remains, but they can't unlock at boot

Synchronize User Password

When a user’s password changes, FileVault needs to be synchronized:

# Sync user's FileVault credentials after password change
$ sudo fdesetup sync

# Or for specific user
$ sudo fdesetup changerecovery -user david

Recovery Key Management

View Recovery Key Type

# Check if personal or institutional recovery key is set
$ sudo fdesetup haspersonalrecoverykey
true

$ sudo fdesetup hasinstitutionalrecoverykey
false

Change Recovery Key

# Generate new personal recovery key
$ sudo fdesetup changerecovery -personal -outputplist

# Output new key
# <?xml version="1.0" encoding="UTF-8"?>
# ...
#     <key>RecoveryKey</key>
#     <string>NEW-XXXX-XXXX-XXXX-XXXX-XXXX</string>
# ...

# Change to institutional recovery key
$ sudo fdesetup changerecovery -institutional -certificate /path/to/cert.cer

Using Recovery Key

If you forget your password, use the recovery key at the login screen:

  1. At the login window, enter wrong password three times
  2. Option to use recovery key appears
  3. Enter the 24-character recovery key

From command line (Recovery Mode):

# Boot to Recovery Mode, open Terminal
$ diskutil apfs unlockVolume disk1s1 -passphrase "XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"

FileVault with T2 and Apple Silicon

On Macs with T2 chip or Apple Silicon, FileVault integrates with hardware security:

T2 Macs

# T2 provides hardware encryption keys
# FileVault adds additional software layer

# Check secure boot status (affects FileVault)
$ sudo bputil -d

# On T2, encryption keys are tied to Secure Enclave
# Data is always encrypted at hardware level
# FileVault adds authentication requirement

Apple Silicon Macs

# Apple Silicon always encrypts data
# FileVault adds authentication layer

# Check encryption status
$ diskutil apfs list

# Data Protection Class:
# Class A - Protected Until First User Authentication
# Class B - Protected Unless Open
# Class C - Protected Until First User Authentication (default)
# Class D - No Protection

Monitoring Encryption Progress

# Monitor initial encryption
$ while true; do
    status=$(fdesetup status)
    echo "$(date): $status"
    if echo "$status" | grep -q "complete"; then
        break
    fi
    sleep 60
done

# More detailed progress
$ diskutil apfs list | grep -A5 "Encryption"

# Watch encryption progress
$ watch -n 30 'fdesetup status'

Scripting FileVault Management

Enable FileVault Silently

#!/bin/bash
# enable-filevault.sh - Enable FileVault programmatically

USERNAME="admin"
PASSWORD="$1"  # Pass password as argument
OUTPUT_DIR="/var/log/filevault"

if [[ -z "$PASSWORD" ]]; then
    echo "Usage: $0 <admin-password>"
    exit 1
fi

# Check if already enabled
if fdesetup status | grep -q "FileVault is On"; then
    echo "FileVault is already enabled"
    exit 0
fi

# Create output directory
mkdir -p "$OUTPUT_DIR"

# Create input plist
PLIST=$(mktemp)
cat > "$PLIST" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Username</key>
    <string>$USERNAME</string>
    <key>Password</key>
    <string>$PASSWORD</string>
</dict>
</plist>
EOF

# Enable FileVault
sudo fdesetup enable -inputplist < "$PLIST" -outputplist > "$OUTPUT_DIR/recovery-key.plist"

# Secure the recovery key file
chmod 600 "$OUTPUT_DIR/recovery-key.plist"

# Clean up
rm -f "$PLIST"

# Verify
fdesetup status

echo "Recovery key saved to $OUTPUT_DIR/recovery-key.plist"
echo "IMPORTANT: Secure this file immediately!"

Audit FileVault Status

#!/bin/bash
# filevault-audit.sh - Audit FileVault configuration

echo "=== FileVault Security Audit ==="
echo "Date: $(date)"
echo "Hostname: $(hostname)"
echo

echo "--- FileVault Status ---"
fdesetup status

echo
echo "--- Enabled Users ---"
sudo fdesetup list 2>/dev/null || echo "Unable to list users (requires admin)"

echo
echo "--- Recovery Key Status ---"
echo -n "Personal Recovery Key: "
sudo fdesetup haspersonalrecoverykey 2>/dev/null || echo "Unknown"
echo -n "Institutional Recovery Key: "
sudo fdesetup hasinstitutionalrecoverykey 2>/dev/null || echo "Unknown"

echo
echo "--- Volume Encryption ---"
diskutil apfs list | grep -A3 "FileVault"

echo
echo "--- Hardware Security ---"
if system_profiler SPiBridgeDataType 2>/dev/null | grep -q "T2"; then
    echo "T2 Security Chip: Present"
elif [[ $(uname -m) == "arm64" ]]; then
    echo "Apple Silicon: Yes"
else
    echo "Hardware Security: Not detected"
fi

echo
echo "=== Audit Complete ==="

Troubleshooting FileVault

FileVault Won’t Enable

# Check for issues
$ sudo fdesetup status -v

# Verify secure token
$ sysadminctl -secureTokenStatus admin
Secure token is ENABLED for user "admin"

# Users need secure token to enable FileVault
# Grant secure token
$ sudo sysadminctl -adminUser admin -adminPassword - \
    -secureTokenOn newuser -password -

Secure Token Issues

# Check which users have secure tokens
$ sudo fdesetup list

# Bootstrap token (for MDM environments)
$ sudo profiles status -type bootstraptoken
Bootstrap Token escrowed to server: NO

# Create bootstrap token
$ sudo profiles install -type bootstraptoken

Encryption Stalled

# Check for errors
$ sudo fdesetup status

# View system logs
$ log show --predicate 'subsystem == "com.apple.corestorage"' --last 1h

# Force encryption to continue
$ sudo fdesetup haspersonalrecoverykey
# This can sometimes restart stalled encryption

Recovery Mode Unlock

# Boot to Recovery Mode
# Open Terminal from Utilities menu

# List volumes
$ diskutil list

# Unlock FileVault volume
$ diskutil apfs unlockVolume disk1s1

# Or with recovery key
$ diskutil apfs unlockVolume disk1s1 -passphrase "XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"

Best Practices

Enable FileVault

Every Mac with sensitive data should have FileVault enabled:

# Verify FileVault is on
$ fdesetup status
FileVault is On.

Secure Recovery Key

  • Store recovery key in secure location (password manager, safe)
  • For enterprises, escrow to MDM or use institutional key
  • Never store recovery key on the encrypted disk

Multiple FileVault Users

  • Enable FileVault for all users who need to boot the Mac
  • Remove departed employees from FileVault
  • Regularly audit enabled users
# Regular audit
$ sudo fdesetup list

Test Recovery

Periodically verify recovery key works:

  1. Boot to Recovery Mode
  2. Open Terminal
  3. Test unlock with recovery key
  4. Lock volume again
  5. Boot normally

Summary

FileVault provides essential full-disk encryption for macOS:

CommandPurpose
fdesetup statusCheck FileVault status
fdesetup enableEnable FileVault
fdesetup disableDisable FileVault
fdesetup listList enabled users
fdesetup addAdd FileVault user
fdesetup removeRemove FileVault user
fdesetup changerecoveryChange recovery key

Key concepts:

  • FileVault encrypts the entire startup disk
  • Users must be enabled to unlock at boot
  • Recovery key allows access if password forgotten
  • T2/Apple Silicon adds hardware encryption layer
  • Always securely store recovery key off-device
# Quick reference
$ fdesetup status                          # Check status
$ sudo fdesetup enable -outputplist        # Enable and get key
$ sudo fdesetup list                       # List enabled users
$ sudo fdesetup add -usertoadd newuser     # Add user
$ diskutil apfs list | grep FileVault      # Check APFS encryption

FileVault should be enabled on every Mac containing sensitive data. With proper key management, it provides strong protection against data theft.

Privacy Controls and TCC Database

TCC (Transparency, Consent, and Control) is the macOS framework that manages privacy permissions. When an application wants to access your camera, microphone, contacts, location, or other sensitive data, TCC mediates the request and prompts you for consent. Understanding TCC is essential for troubleshooting permission issues, managing privacy settings programmatically, and auditing application access.

How TCC Works

┌─────────────────────────────────────────────────────────────────┐
│                    TCC Architecture                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   Application                                                    │
│       │                                                         │
│       ▼                                                         │
│   Framework API Request                                         │
│   (e.g., AVCaptureDevice for camera)                            │
│       │                                                         │
│       ▼                                                         │
│   ┌────────────────────────────────────┐                       │
│   │          tccd daemon               │                       │
│   │  (TCC database + policy engine)    │                       │
│   └────────────────────────────────────┘                       │
│       │                                                         │
│       ├── Check TCC.db for existing permission                  │
│       │                                                         │
│       ├── If no permission: Show consent dialog                 │
│       │                                                         │
│       └── Return: Granted / Denied / Not Determined             │
│                                                                  │
│   TCC Databases:                                                 │
│   - User: ~/Library/Application Support/com.apple.TCC/TCC.db   │
│   - System: /Library/Application Support/com.apple.TCC/TCC.db  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

TCC Protected Services

TCC protects numerous categories of sensitive data:

ServiceDescription
CameraAccess to camera device
MicrophoneAccess to microphone
Screen RecordingCapture screen content
AccessibilityControl UI of other apps
Full Disk AccessAccess all files
ContactsAddress book data
CalendarCalendar events
RemindersReminders data
PhotosPhoto library access
Location ServicesGeographic location
System EventsApple Events automation
Files and FoldersSpecific folder access
AutomationControl other applications
Input MonitoringMonitor keyboard/mouse
Media & Apple MusicApple Music library
Developer ToolsDebugging other apps

The tccutil Command

The tccutil command can reset TCC permissions for applications.

Resetting Permissions

# Reset all permissions for an application
$ tccutil reset All com.example.app

# Reset specific service
$ tccutil reset Camera com.example.app
$ tccutil reset Microphone com.example.app
$ tccutil reset ScreenCapture com.example.app
$ tccutil reset Accessibility com.example.app

# Reset for all applications (service-wide)
$ tccutil reset Camera

# Reset all services for all apps (use carefully)
$ tccutil reset All

Available Services

# Common service identifiers for tccutil
AddressBook              # Contacts
Calendar                 # Calendar
Reminders                # Reminders
Photos                   # Photo Library
Camera                   # Camera
Microphone               # Microphone
Accessibility            # Accessibility
PostEvent                # System Events
ScreenCapture            # Screen Recording
MediaLibrary             # Media & Apple Music
ListenEvent              # Input Monitoring
SystemPolicyAllFiles     # Full Disk Access
SystemPolicySysAdminFiles # Administer files
SystemPolicyDeveloperFiles # Developer Files
AppleEvents              # Automation

Reset Examples

# App has wrong camera permissions
$ tccutil reset Camera com.zoom.us
# Next time Zoom requests camera, user will be prompted

# Reset Terminal's Full Disk Access
$ tccutil reset SystemPolicyAllFiles com.apple.Terminal

# Clear all automation permissions
$ tccutil reset AppleEvents

# Clear all permissions for a malware removal
$ tccutil reset All com.suspicious.app

Viewing TCC Database

The TCC database stores permission decisions. Accessing it requires Full Disk Access.

User TCC Database

# Location
$ ls ~/Library/Application\ Support/com.apple.TCC/TCC.db

# View with sqlite3 (requires Full Disk Access)
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT client, service, auth_value FROM access"

# More readable output
$ sqlite3 -header -column ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT client, service, auth_value,
          datetime(last_modified, 'unixepoch') as modified
   FROM access
   ORDER BY service, client"

# auth_value meanings:
# 0 = Denied
# 1 = Unknown (ask next time)
# 2 = Allowed
# 3 = Limited

# Filter by service
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT client, auth_value FROM access WHERE service = 'kTCCServiceCamera'"

System TCC Database

# Requires admin privileges
$ sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT client, service, auth_value FROM access"

# System-level permissions include:
# - Accessibility permissions
# - Input monitoring
# - Screen recording (some cases)

TCC Database Schema

-- Main access table structure (simplified)
CREATE TABLE access (
    service TEXT NOT NULL,        -- Service identifier
    client TEXT NOT NULL,         -- App bundle ID or path
    client_type INTEGER NOT NULL, -- 0=bundle, 1=path
    auth_value INTEGER NOT NULL,  -- Permission state
    auth_reason INTEGER NOT NULL, -- How granted
    auth_version INTEGER NOT NULL,
    csreq BLOB,                   -- Code signing requirement
    policy_id INTEGER,
    indirect_object_identifier TEXT,
    indirect_object_code_identity BLOB,
    flags INTEGER,
    last_modified INTEGER,        -- Unix timestamp
    PRIMARY KEY (service, client, client_type, indirect_object_identifier)
);

Querying Specific Permissions

#!/bin/bash
# check-tcc.sh - Check TCC permissions for an app

APP_ID="$1"
if [[ -z "$APP_ID" ]]; then
    echo "Usage: $0 <bundle-identifier>"
    exit 1
fi

DB="$HOME/Library/Application Support/com.apple.TCC/TCC.db"

echo "=== TCC Permissions for $APP_ID ==="
sqlite3 -header -column "$DB" \
  "SELECT service,
          CASE auth_value
              WHEN 0 THEN 'Denied'
              WHEN 1 THEN 'Unknown'
              WHEN 2 THEN 'Allowed'
              WHEN 3 THEN 'Limited'
              ELSE auth_value
          END as status,
          datetime(last_modified, 'unixepoch') as modified
   FROM access
   WHERE client = '$APP_ID'
   ORDER BY service"

Privacy Preferences in System Settings

Check via Profiles

# Export privacy preferences (MDM-managed)
$ sudo profiles -P | grep -i privacy

# View privacy configuration profiles
$ sudo profiles list -verbose

Using System Preferences CLI

# Open Privacy settings
$ open "x-apple.systempreferences:com.apple.preference.security?Privacy"

# Open specific privacy pane
$ open "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera"
$ open "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone"
$ open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
$ open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"

Granting TCC Permissions

For Terminal/CLI Tools

Terminal needs Full Disk Access for many operations:

  1. Open System Preferences > Security & Privacy > Privacy
  2. Select “Full Disk Access”
  3. Click the lock to make changes
  4. Add Terminal.app (or your terminal emulator)
  5. Restart Terminal
# After granting Full Disk Access, you can:
$ ls ~/Library/Mail/              # Access Mail data
$ sqlite3 ~/Library/Messages/chat.db  # Access Messages
$ cat ~/Library/Safari/History.db  # Access Safari history

For Automation/AppleScript

# Script that requires automation permission
$ osascript -e 'tell application "System Events" to keystroke "a"'
# Will prompt for Accessibility permission

# Check if permission exists
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT auth_value FROM access
   WHERE service='kTCCServiceAppleEvents'
   AND client='com.apple.Terminal'"

MDM-Based Permission Grants

For enterprise deployment, use Privacy Preferences Policy Control profiles:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PayloadContent</key>
    <array>
        <dict>
            <key>PayloadType</key>
            <string>com.apple.TCC.configuration-profile-policy</string>
            <key>Services</key>
            <dict>
                <key>SystemPolicyAllFiles</key>
                <array>
                    <dict>
                        <key>Allowed</key>
                        <true/>
                        <key>CodeRequirement</key>
                        <string>identifier "com.example.app" and anchor apple generic</string>
                        <key>IdentifierType</key>
                        <string>bundleID</string>
                        <key>Identifier</key>
                        <string>com.example.app</string>
                    </dict>
                </array>
            </dict>
        </dict>
    </array>
</dict>
</plist>

Programmatic Permission Requests

Checking Permission Status

# From an app's perspective, permissions have states:
# - Not Determined: Never asked
# - Restricted: Disabled by policy (e.g., parental controls)
# - Denied: User denied permission
# - Authorized: User granted permission

# Example: Check camera authorization (from app code)
# AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];

Permission States in TCC

# Query authorization states
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT client,
          CASE auth_value
              WHEN 0 THEN 'Denied'
              WHEN 1 THEN 'Not Determined'
              WHEN 2 THEN 'Authorized'
              WHEN 3 THEN 'Limited (Photos)'
          END as status
   FROM access
   WHERE service = 'kTCCServiceCamera'"

Automation Permissions

Automation (controlling other apps) has its own TCC category:

# Check automation permissions
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT client, indirect_object_identifier, auth_value
   FROM access
   WHERE service = 'kTCCServiceAppleEvents'"

# Example: Terminal controlling System Events
# client = com.apple.Terminal
# indirect_object_identifier = com.apple.systemevents
# auth_value = 2 (allowed)

Script Requiring Automation

#!/bin/bash
# This script requires automation permission

# Tell Finder to empty trash
osascript << 'EOF'
tell application "Finder"
    empty trash
end tell
EOF

# First run will prompt for permission
# Grant Terminal permission to control Finder

Privacy Audit Script

#!/bin/bash
# tcc-audit.sh - Audit TCC permissions

USER_DB="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
SYSTEM_DB="/Library/Application Support/com.apple.TCC/TCC.db"

echo "=== TCC Privacy Audit ==="
echo "Date: $(date)"
echo

# Function to decode auth_value
decode_auth() {
    case $1 in
        0) echo "Denied" ;;
        1) echo "Unknown" ;;
        2) echo "Allowed" ;;
        3) echo "Limited" ;;
        *) echo "$1" ;;
    esac
}

echo "--- User TCC Database ---"
if [[ -r "$USER_DB" ]]; then
    sqlite3 -header -column "$USER_DB" \
      "SELECT service, client, auth_value as auth,
              datetime(last_modified, 'unixepoch') as modified
       FROM access
       ORDER BY service, client" 2>/dev/null || echo "Cannot read (need Full Disk Access)"
else
    echo "Cannot access (need Full Disk Access for Terminal)"
fi

echo
echo "--- System TCC Database ---"
sudo sqlite3 -header -column "$SYSTEM_DB" \
  "SELECT service, client, auth_value as auth
   FROM access
   ORDER BY service, client" 2>/dev/null || echo "Cannot read"

echo
echo "--- Camera Access ---"
sqlite3 "$USER_DB" \
  "SELECT client FROM access WHERE service='kTCCServiceCamera' AND auth_value=2" 2>/dev/null

echo
echo "--- Microphone Access ---"
sqlite3 "$USER_DB" \
  "SELECT client FROM access WHERE service='kTCCServiceMicrophone' AND auth_value=2" 2>/dev/null

echo
echo "--- Screen Recording Access ---"
sqlite3 "$USER_DB" \
  "SELECT client FROM access WHERE service='kTCCServiceScreenCapture' AND auth_value=2" 2>/dev/null

echo
echo "--- Full Disk Access ---"
sqlite3 "$USER_DB" \
  "SELECT client FROM access WHERE service='kTCCServiceSystemPolicyAllFiles' AND auth_value=2" 2>/dev/null

echo
echo "--- Accessibility Access ---"
sudo sqlite3 "$SYSTEM_DB" \
  "SELECT client FROM access WHERE service='kTCCServiceAccessibility' AND auth_value=2" 2>/dev/null

echo
echo "=== Audit Complete ==="

Troubleshooting TCC Issues

App Not Prompting for Permission

# Reset permission to force re-prompt
$ tccutil reset Camera com.example.app

# Verify the app is code signed
$ codesign -dv /Applications/SomeApp.app

# Check if app has required Info.plist keys
$ /usr/libexec/PlistBuddy -c "Print :NSCameraUsageDescription" \
    /Applications/SomeApp.app/Contents/Info.plist

Permission Granted But Not Working

# Verify permission in database
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT * FROM access WHERE client='com.example.app'"

# Check code signing requirement matches
$ codesign -dr - /Applications/SomeApp.app

# Restart the tccd daemon
$ sudo killall -9 tccd
# tccd restarts automatically

Full Disk Access Not Working

# Ensure Terminal is in FDA list
# Check TCC database
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT * FROM access WHERE service='kTCCServiceSystemPolicyAllFiles' AND client='com.apple.Terminal'"

# If using different terminal (iTerm, etc.)
# Add that terminal app to Full Disk Access

# Restart Terminal after granting permission

Viewing TCC Logs

# View TCC-related logs
$ log show --predicate 'subsystem == "com.apple.TCC"' --last 10m

# Stream TCC logs
$ log stream --predicate 'subsystem == "com.apple.TCC"'

# Filter for denials
$ log show --predicate 'subsystem == "com.apple.TCC" AND eventMessage CONTAINS "denied"' --last 1h

Security Implications

Malware and TCC

Malware may attempt to bypass TCC:

  • Exploit vulnerabilities in tccd
  • Abuse accessibility permissions
  • Use synthetic clicks to grant permissions
# Check for suspicious TCC entries
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT * FROM access WHERE auth_value=2" | grep -v "com.apple"

# Look for unknown apps with sensitive permissions
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "SELECT client FROM access
   WHERE service='kTCCServiceAccessibility' AND auth_value=2"

Hardening TCC

  1. Regularly audit TCC permissions
  2. Remove unused application permissions
  3. Be cautious granting Accessibility access
  4. Use MDM to control permissions in enterprise

Summary

TCC manages privacy permissions on macOS:

CommandPurpose
tccutil resetReset permissions
sqlite3 TCC.dbQuery permission database
System PreferencesGUI permission management
MDM ProfilesEnterprise permission control

Key concepts:

  • TCC mediates access to sensitive data
  • Permissions stored in SQLite database
  • User and system databases exist
  • Apps must declare usage descriptions
  • Permissions can be reset via tccutil
# Quick reference
$ tccutil reset Camera com.example.app     # Reset camera permission
$ tccutil reset All com.example.app        # Reset all permissions
$ sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db "SELECT * FROM access"  # View permissions
$ log show --predicate 'subsystem == "com.apple.TCC"' --last 10m  # View TCC logs

TCC is a critical privacy protection layer. Understanding it helps diagnose permission issues and audit what data applications can access.

Secure Boot on T2 and Apple Silicon

Modern Macs include dedicated security hardware that ensures the boot process is protected from tampering. Starting with the T2 Security Chip (2017) and continuing with Apple Silicon (2020), Macs verify every stage of the boot process cryptographically. Understanding these security features is essential for IT administrators managing Mac fleets and developers who need to load custom kernel extensions.

Hardware Security Overview

┌─────────────────────────────────────────────────────────────────┐
│                    Mac Security Architecture                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌─────────────────────┐    ┌─────────────────────┐           │
│   │    Main Processor   │    │   Security Chip     │           │
│   │    (Intel/Apple)    │    │   (T2/Secure Encl)  │           │
│   │                     │    │                     │           │
│   │   macOS / Apps      │    │  - Boot ROM         │           │
│   │                     │    │  - Secure Boot      │           │
│   │                     │◄───│  - Touch ID         │           │
│   │                     │    │  - Encryption Keys  │           │
│   │                     │    │  - System Integrity │           │
│   └─────────────────────┘    └─────────────────────┘           │
│                                                                  │
│   Secure Boot Chain:                                            │
│   Boot ROM ─▶ iBoot ─▶ macOS Kernel ─▶ System Extensions       │
│      │          │           │                │                  │
│      └──────────┴───────────┴────────────────┘                  │
│           Each stage verifies the next                          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

T2 Security Chip

The T2 chip (found in Intel Macs from 2018-2020) provides:

Features

FeatureDescription
Secure BootVerifies boot process integrity
Encrypted StorageHardware encryption keys
Touch IDBiometric authentication
Secure EnclaveIsolated security processor
Image Signal ProcessorCamera processing
Audio ControllerMicrophone disconnect

Checking T2 Status

# Check if Mac has T2 chip
$ system_profiler SPiBridgeDataType
Apple T2 Security Chip:

  Model Name: Apple T2 Security Chip
  Model Identifier: T2Chip1,1
  ...

# Alternative check
$ ioreg -l | grep "T2" | head -1

# Check T2 firmware version
$ system_profiler SPiBridgeDataType | grep "Firmware Version"

Apple Silicon Security

Apple Silicon Macs (M1, M2, M3, etc.) integrate security directly into the main chip:

Features

FeatureDescription
Secure EnclaveIsolated security coprocessor
Boot ROMHardware root of trust
Secure BootCryptographic verification
Pointer AuthenticationCode integrity protection
Kernel Integrity ProtectionRuntime kernel verification
Device IsolationHardware memory protection

Checking Apple Silicon

# Check processor type
$ uname -m
arm64

# View Apple Silicon details
$ system_profiler SPHardwareDataType | grep -i "chip"
      Chip: Apple M1 Pro

# View security features
$ sysctl -a | grep -i "arm64"

Boot Security Modes

Both T2 and Apple Silicon Macs support different security levels:

Security Levels

LevelDescriptionUse Case
Full SecurityOnly Apple-signed softwareDefault, recommended
Reduced SecurityAllows notarized kextsThird-party kexts
Permissive SecurityNo verification (Apple Silicon)Development only

Checking Security Mode

# On Apple Silicon
$ sudo bputil -d
Current OS environment:
  OS Type: macOS
  Boot Mode: Normal
  Security Mode: Full Security
  ...

# Alternative (works on both)
$ csrutil status
System Integrity Protection status: enabled.

# Detailed Apple Silicon security
$ sudo bputil -s

Startup Security Utility

Accessing on Intel T2 Macs

  1. Restart Mac
  2. Hold Command + R during startup
  3. Select Utilities > Startup Security Utility
  4. Authenticate with admin credentials

Accessing on Apple Silicon Macs

  1. Shut down Mac completely
  2. Press and hold Power button until “Loading startup options”
  3. Click Options > Continue
  4. Select Utilities > Startup Security Utility

Command-Line Equivalents

# View current security policy (Apple Silicon)
$ sudo bputil -d

# Change security policy (Recovery Mode required)
# From Terminal in Recovery Mode:

# Set Full Security
$ sudo bputil --full-security

# Set Reduced Security
$ sudo bputil --reduced-security

# Enable kernel extension loading (Reduced Security)
$ sudo bputil --reduced-security --allow-kexts

# Enable MDM management
$ sudo bputil --reduced-security --allow-mdm-enrollment

# Reset to defaults
$ sudo bputil --default

Firmware Password

A firmware password prevents unauthorized users from starting from different disks or entering Recovery Mode.

Setting Firmware Password (Intel Macs)

# Check firmware password status
$ sudo firmwarepasswd -check
Password Enabled: No

# Set firmware password (prompts for password)
$ sudo firmwarepasswd -setpasswd
Enter new password:
Re-enter new password:
Password set successfully.

# Verify
$ sudo firmwarepasswd -check
Password Enabled: Yes

# Delete firmware password
$ sudo firmwarepasswd -delete
Enter password:
Password removed successfully.

# Change firmware password
$ sudo firmwarepasswd -setpasswd

Firmware Password Modes

# Set to command mode (requires password for startup key combos)
$ sudo firmwarepasswd -setmode command
# Protects: Option boot, Recovery Mode, etc.

# Set to full mode (requires password for any boot)
$ sudo firmwarepasswd -setmode full
# Protects: All startup scenarios

Apple Silicon Recovery Password

Apple Silicon Macs don’t have traditional firmware passwords. Instead:

  1. Recovery Mode requires authentication with a user account
  2. Activation Lock (tied to Apple ID) provides additional protection
  3. MDM can set restrictions
# On Apple Silicon, view security settings
$ sudo bputil -d

# Activation Lock status (check in System Information)
$ system_profiler SPHardwareDataType | grep "Activation Lock"

Secure Boot Policies

Local Policy (Apple Silicon)

Each bootable OS has its own security policy:

# View all boot policies
$ sudo bputil -l

# View policy for specific OS
$ sudo bputil -d --volume-path /Volumes/MacintoshHD

# OS security policies include:
# - Signature requirements
# - Auxiliary kernel extensions
# - MDM enrollment status

Boot Policy Changes

# In Recovery Mode Terminal:

# Allow user management of kernel extensions
$ sudo bputil -g

# The -g flag enables:
# - Loading third-party kexts
# - Reduced security for the OS

# After running, restart and:
# System Preferences > Security > Allow apps from identified developers

Kernel Extension Loading

Modern Macs restrict kernel extension loading for security:

Kext Loading on T2 Macs

# Check kext loading policy
$ sudo spctl kext-consent status
Kernel Extension User Consent: ENABLED

# Add team ID to allowed list (must be in Recovery Mode)
$ sudo spctl kext-consent add TEAMID12345

# List allowed team IDs
$ sudo spctl kext-consent list

# Disable user consent (not recommended)
$ sudo spctl kext-consent disable

Kext Loading on Apple Silicon

# Must be in Reduced Security mode
$ sudo bputil --reduced-security

# Then from normal boot, approve kexts in:
# System Preferences > Security & Privacy

# Check loaded kexts
$ kextstat | head -10

# View kext loading errors
$ sudo kextutil -v /path/to/kext.kext

System Extensions (Modern Alternative)

Apple encourages System Extensions over kernel extensions:

# List system extensions
$ systemextensionsctl list

# System extensions run in user space
# Safer than kernel extensions
# Required for App Store apps

# Uninstall system extension
$ systemextensionsctl uninstall TEAMID com.example.extension

External Boot

Control whether the Mac can boot from external devices:

T2 Macs

# In Startup Security Utility (Recovery Mode):
# "Allowed Boot Media":
# - Full Security: Only internal disk
# - Medium Security: Allows Apple-signed external media
# - No Security: Allows any bootable external media

Apple Silicon

# External boot requires:
# 1. Reduced Security mode
# 2. Explicit approval for external media

# In Recovery Mode:
$ sudo bputil --reduced-security

# Then select "Security Policy" for external volume
# and approve in Startup Security Utility

Startup Key Combinations

Key combinations available at boot (may require firmware password):

KeysFunction
Option (Alt)Startup Manager
Command + RRecovery Mode
Command + Option + RInternet Recovery
ShiftSafe Mode
DApple Diagnostics
NNetwork Startup
TTarget Disk Mode (Intel)
Command + VVerbose Mode
# Note: On Apple Silicon, hold Power button instead for Recovery

Security Audit Commands

#!/bin/bash
# boot-security-audit.sh - Audit Mac boot security

echo "=== Boot Security Audit ==="
echo "Date: $(date)"
echo

echo "--- Hardware Type ---"
if [[ $(uname -m) == "arm64" ]]; then
    echo "Platform: Apple Silicon"
    system_profiler SPHardwareDataType | grep "Chip"
elif system_profiler SPiBridgeDataType 2>/dev/null | grep -q "T2"; then
    echo "Platform: Intel with T2"
    system_profiler SPiBridgeDataType | grep "Model Name"
else
    echo "Platform: Intel (No T2)"
fi

echo
echo "--- SIP Status ---"
csrutil status

echo
echo "--- Secure Boot Policy ---"
if [[ $(uname -m) == "arm64" ]]; then
    sudo bputil -d 2>/dev/null || echo "Run with sudo for detailed info"
fi

echo
echo "--- Firmware Password ---"
if [[ $(uname -m) != "arm64" ]]; then
    sudo firmwarepasswd -check 2>/dev/null || echo "Unknown"
else
    echo "N/A (Apple Silicon uses Recovery authentication)"
fi

echo
echo "--- Kext Consent ---"
sudo spctl kext-consent status 2>/dev/null

echo
echo "--- Gatekeeper ---"
spctl --status

echo
echo "--- FileVault ---"
fdesetup status

echo
echo "--- System Extensions ---"
systemextensionsctl list 2>/dev/null | head -10

echo
echo "=== Audit Complete ==="

Troubleshooting Boot Security

Can’t Boot from External Drive

# 1. Check security settings in Recovery Mode
# 2. Ensure external drive is properly formatted (APFS)
# 3. For Apple Silicon, approve external boot policy

# Format external drive
$ diskutil eraseDisk APFS "External" GPT disk2

Kext Won’t Load

# Check kext is properly signed
$ codesign -dv /path/to/kext.kext

# Check team ID is allowed
$ sudo spctl kext-consent list

# View kext loading errors
$ log show --predicate 'process == "kernel"' --last 5m | grep -i kext

# Ensure proper security mode
$ csrutil status
$ sudo bputil -d  # Apple Silicon

Boot Stuck or Failing

# Boot to Recovery Mode
# Use Disk Utility to verify/repair disk
# Check Console for boot errors

# Safe Mode boot (Shift key at startup)
# Disables non-essential kexts

# Verbose Mode (Command + V)
# Shows boot messages for diagnosis

Enterprise Management

MDM and Secure Boot

# Check MDM enrollment
$ profiles status -type enrollment
Enrolled via DEP: Yes
MDM enrollment: Yes (User Approved)

# Bootstrap token status (for FileVault and kext management)
$ sudo profiles status -type bootstraptoken

# MDM can configure:
# - Boot security policy
# - Allowed kernel extensions
# - FileVault escrow
# - Activation Lock

Automated Security Configuration

#!/bin/bash
# enterprise-security-setup.sh - Configure enterprise security

# Verify MDM enrollment
if ! profiles status -type enrollment | grep -q "Yes"; then
    echo "Warning: Device not MDM enrolled"
    exit 1
fi

# Enable FileVault (with MDM escrow)
if fdesetup status | grep -q "Off"; then
    sudo fdesetup enable -defer /tmp/fv-key.plist
fi

# Verify SIP is enabled
if ! csrutil status | grep -q "enabled"; then
    echo "Warning: SIP is disabled"
fi

# Verify Gatekeeper
if ! spctl --status | grep -q "enabled"; then
    sudo spctl --master-enable
fi

echo "Security configuration complete"

Summary

Secure Boot provides hardware-backed protection for Mac startup:

ComponentT2 MacsApple Silicon
Security ChipT2Integrated
Secure BootYesYes
Boot ROMT2Main chip
Firmware PasswordYesRecovery auth
Security ModesFull/Medium/NoFull/Reduced/Permissive
External Boot ControlYesYes

Key commands:

# Check security status
$ csrutil status                       # SIP status
$ sudo bputil -d                       # Boot policy (Apple Silicon)
$ sudo firmwarepasswd -check           # Firmware password (Intel)
$ sudo spctl kext-consent status       # Kext consent

# Recovery Mode operations
$ sudo bputil --full-security          # Set full security
$ sudo bputil --reduced-security       # Allow kexts
$ sudo spctl kext-consent add TEAMID   # Allow kext developer

Secure Boot ensures that your Mac starts with verified, trusted software from the moment it powers on, forming the foundation of macOS security.

Security Best Practices

Securing macOS requires a layered approach that combines Apple’s built-in security features with good operational practices. This chapter provides a comprehensive hardening checklist, introduces security auditing tools, and outlines monitoring strategies for maintaining security over time.

Security Hardening Checklist

Essential Security (All Macs)

#!/bin/bash
# essential-security-check.sh - Verify essential security settings

echo "=== Essential Security Checklist ==="

# 1. FileVault
echo -n "[1/10] FileVault: "
if fdesetup status | grep -q "On"; then
    echo "ENABLED"
else
    echo "DISABLED - Enable in System Preferences > Security & Privacy > FileVault"
fi

# 2. System Integrity Protection
echo -n "[2/10] SIP: "
if csrutil status | grep -q "enabled"; then
    echo "ENABLED"
else
    echo "DISABLED - Enable via Recovery Mode"
fi

# 3. Gatekeeper
echo -n "[3/10] Gatekeeper: "
if spctl --status | grep -q "enabled"; then
    echo "ENABLED"
else
    echo "DISABLED - Run: sudo spctl --master-enable"
fi

# 4. Firewall
echo -n "[4/10] Firewall: "
FW=$(/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null)
if echo "$FW" | grep -q "enabled"; then
    echo "ENABLED"
else
    echo "DISABLED - Enable in System Preferences > Security & Privacy > Firewall"
fi

# 5. Auto-login disabled
echo -n "[5/10] Auto-login: "
if defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null; then
    echo "ENABLED - SECURITY RISK"
else
    echo "DISABLED (good)"
fi

# 6. Password after sleep
echo -n "[6/10] Password on wake: "
SCREEN_SAVER=$(defaults read com.apple.screensaver askForPassword 2>/dev/null)
if [[ "$SCREEN_SAVER" == "1" ]]; then
    echo "ENABLED"
else
    echo "DISABLED - Enable in System Preferences > Security & Privacy"
fi

# 7. Remote login
echo -n "[7/10] SSH: "
if sudo systemsetup -getremotelogin 2>/dev/null | grep -q "Off"; then
    echo "DISABLED (good unless needed)"
else
    echo "ENABLED - Verify this is intentional"
fi

# 8. Screen sharing
echo -n "[8/10] Screen Sharing: "
if launchctl list | grep -q screensharing; then
    echo "ENABLED - Verify this is intentional"
else
    echo "DISABLED (good)"
fi

# 9. Find My Mac
echo -n "[9/10] Find My Mac: "
if defaults read ~/Library/Preferences/com.apple.finder.plist FXEnableFindMyMac 2>/dev/null | grep -q "1"; then
    echo "ENABLED"
else
    echo "Check in System Preferences > Apple ID > iCloud"
fi

# 10. Software updates
echo -n "[10/10] Auto Updates: "
if defaults read /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled 2>/dev/null | grep -q "1"; then
    echo "ENABLED"
else
    echo "DISABLED - Enable automatic updates"
fi

echo "=== Checklist Complete ==="

Network Security

#!/bin/bash
# network-security-check.sh - Audit network security settings

echo "=== Network Security Audit ==="

# Firewall details
echo "--- Firewall Configuration ---"
/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
/usr/libexec/ApplicationFirewall/socketfilterfw --getblockall
/usr/libexec/ApplicationFirewall/socketfilterfw --getstealthmode

# Listening services
echo
echo "--- Listening Services ---"
sudo lsof -iTCP -sTCP:LISTEN -n -P | awk 'NR==1 || $9 !~ /127\.0\.0\.1|::1/' | head -20

# Sharing services
echo
echo "--- Sharing Services ---"
echo -n "Remote Login (SSH): "
sudo systemsetup -getremotelogin 2>/dev/null

echo -n "Screen Sharing: "
sudo launchctl list | grep -q com.apple.screensharing && echo "Enabled" || echo "Disabled"

echo -n "File Sharing (SMB): "
sudo launchctl list | grep -q com.apple.smbd && echo "Enabled" || echo "Disabled"

echo -n "Remote Apple Events: "
sudo systemsetup -getremoteappleevents 2>/dev/null

# Network interfaces
echo
echo "--- Active Interfaces ---"
ifconfig | grep -E "^[a-z]|inet " | grep -v "127.0.0.1"

echo "=== Network Audit Complete ==="

Application Security

#!/bin/bash
# app-security-check.sh - Audit application security

echo "=== Application Security Audit ==="

# Check for unsigned applications
echo "--- Unsigned Applications in /Applications ---"
for app in /Applications/*.app; do
    if ! codesign -v "$app" 2>/dev/null; then
        echo "UNSIGNED: $app"
    fi
done

# Check Gatekeeper assessments
echo
echo "--- Gatekeeper Assessment Failures ---"
for app in /Applications/*.app; do
    result=$(spctl --assess -v "$app" 2>&1)
    if echo "$result" | grep -q "rejected"; then
        echo "REJECTED: $app"
    fi
done

# Check for quarantined files
echo
echo "--- Quarantined Files in Downloads ---"
find ~/Downloads -xattr -print0 2>/dev/null | xargs -0 -I {} sh -c 'xattr -l "{}" 2>/dev/null | grep -q quarantine && echo "{}"' | head -20

# Browser extensions (common locations)
echo
echo "--- Installed Browser Extensions ---"
ls ~/Library/Application\ Support/Google/Chrome/Default/Extensions/ 2>/dev/null | wc -l | xargs echo "Chrome extensions:"
ls ~/Library/Safari/Extensions/ 2>/dev/null | wc -l | xargs echo "Safari extensions:"

echo "=== Application Audit Complete ==="

Security Configuration Scripts

Enable Firewall

#!/bin/bash
# enable-firewall.sh - Configure application firewall

# Enable firewall
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on

# Enable stealth mode (don't respond to pings)
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on

# Block all incoming (except essential services)
# WARNING: This may break some apps
# sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setblockall on

# Allow signed apps automatically
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setallowsigned on
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setallowsignedapp on

# Log firewall events
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode on

echo "Firewall configured. Restart may be required."

Secure SSH Configuration

#!/bin/bash
# secure-ssh.sh - Harden SSH configuration

# Backup original config
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup

# Create secure configuration
sudo tee /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
# SSH Hardening Configuration

# Disable root login
PermitRootLogin no

# Use SSH protocol 2 only (default in modern OpenSSH)
Protocol 2

# Disable password authentication (use keys)
PasswordAuthentication no
ChallengeResponseAuthentication no

# Limit authentication attempts
MaxAuthTries 3

# Set idle timeout
ClientAliveInterval 300
ClientAliveCountMax 2

# Disable X11 forwarding
X11Forwarding no

# Disable TCP forwarding (if not needed)
AllowTcpForwarding no

# Only allow specific users (customize as needed)
# AllowUsers admin

# Use strong ciphers
Ciphers aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256
EOF

# Restart SSH
sudo launchctl kickstart -k system/com.openssh.sshd

echo "SSH hardened. Test connection before closing current session!"

Secure Safari Settings

#!/bin/bash
# secure-safari.sh - Configure Safari security settings

# Disable auto-open of safe downloads
defaults write com.apple.Safari AutoOpenSafeDownloads -bool false

# Enable fraud warnings
defaults write com.apple.Safari WarnAboutFraudulentWebsites -bool true

# Disable plugins
defaults write com.apple.Safari WebKitPluginsEnabled -bool false
defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2PluginsEnabled -bool false

# Enable Do Not Track
defaults write com.apple.Safari SendDoNotTrackHTTPHeader -bool true

# Block pop-ups
defaults write com.apple.Safari WebKitJavaScriptCanOpenWindowsAutomatically -bool false
defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2JavaScriptCanOpenWindowsAutomatically -bool false

# Disable auto-fill
defaults write com.apple.Safari AutoFillFromAddressBook -bool false
defaults write com.apple.Safari AutoFillPasswords -bool false
defaults write com.apple.Safari AutoFillCreditCardData -bool false

echo "Safari security settings configured. Restart Safari to apply."

Security Monitoring

System Log Monitoring

#!/bin/bash
# security-monitor.sh - Monitor security-related events

echo "=== Security Event Monitor ==="
echo "Press Ctrl+C to stop"
echo

# Monitor security-related logs
log stream --predicate '
    subsystem == "com.apple.securityd" OR
    subsystem == "com.apple.TCC" OR
    subsystem == "com.apple.sandbox" OR
    subsystem == "com.apple.authd" OR
    eventMessage CONTAINS[c] "authentication" OR
    eventMessage CONTAINS[c] "password" OR
    eventMessage CONTAINS[c] "denied" OR
    eventMessage CONTAINS[c] "violation"
' --style compact

Failed Login Monitoring

#!/bin/bash
# failed-logins.sh - Check for failed login attempts

echo "=== Failed Login Attempts (Last 24 Hours) ==="

# Authentication failures
log show --predicate 'process == "authd" AND eventMessage CONTAINS "failed"' --last 24h

# SSH failures (if SSH is enabled)
log show --predicate 'process == "sshd" AND eventMessage CONTAINS "Failed"' --last 24h | tail -20

File Integrity Monitoring

#!/bin/bash
# integrity-monitor.sh - Basic file integrity monitoring

BASELINE_FILE="$HOME/.security/baseline.txt"
ALERT_LOG="$HOME/.security/alerts.log"

mkdir -p "$HOME/.security"

# Critical paths to monitor
PATHS=(
    "/etc/hosts"
    "/etc/passwd"
    "/etc/sudoers"
    "/Library/LaunchDaemons"
    "/Library/LaunchAgents"
    "$HOME/Library/LaunchAgents"
)

generate_baseline() {
    echo "Generating baseline..."
    for path in "${PATHS[@]}"; do
        if [[ -e "$path" ]]; then
            if [[ -d "$path" ]]; then
                find "$path" -type f -exec shasum -a 256 {} \; 2>/dev/null
            else
                shasum -a 256 "$path" 2>/dev/null
            fi
        fi
    done > "$BASELINE_FILE"
    echo "Baseline saved to $BASELINE_FILE"
}

check_integrity() {
    if [[ ! -f "$BASELINE_FILE" ]]; then
        echo "No baseline found. Run with 'baseline' first."
        exit 1
    fi

    echo "Checking file integrity..."
    CHANGES=0

    while IFS= read -r line; do
        hash=$(echo "$line" | awk '{print $1}')
        file=$(echo "$line" | awk '{print $2}')

        if [[ -f "$file" ]]; then
            current_hash=$(shasum -a 256 "$file" | awk '{print $1}')
            if [[ "$hash" != "$current_hash" ]]; then
                echo "MODIFIED: $file"
                echo "$(date): MODIFIED: $file" >> "$ALERT_LOG"
                ((CHANGES++))
            fi
        else
            echo "MISSING: $file"
            echo "$(date): MISSING: $file" >> "$ALERT_LOG"
            ((CHANGES++))
        fi
    done < "$BASELINE_FILE"

    # Check for new files
    for path in "${PATHS[@]}"; do
        if [[ -d "$path" ]]; then
            while IFS= read -r file; do
                if ! grep -q "$file" "$BASELINE_FILE" 2>/dev/null; then
                    echo "NEW: $file"
                    echo "$(date): NEW: $file" >> "$ALERT_LOG"
                    ((CHANGES++))
                fi
            done < <(find "$path" -type f 2>/dev/null)
        fi
    done

    if [[ $CHANGES -eq 0 ]]; then
        echo "No changes detected."
    else
        echo "$CHANGES change(s) detected!"
    fi
}

case "$1" in
    baseline)
        generate_baseline
        ;;
    check)
        check_integrity
        ;;
    *)
        echo "Usage: $0 {baseline|check}"
        exit 1
        ;;
esac

Security Tools

Built-in Tools

ToolPurposeExample
codesignVerify code signaturescodesign -v /Applications/App.app
spctlGatekeeper controlspctl --assess -v /path/to/app
csrutilSIP managementcsrutil status
fdesetupFileVault managementfdesetup status
securityKeychain managementsecurity find-generic-password
logSystem log accesslog show --predicate 'subsystem == "com.apple.securityd"'
tccutilTCC permission resettccutil reset Camera
xattrExtended attributesxattr -d com.apple.quarantine file

Third-Party Security Tools

# Install common security tools via Homebrew

# Network security
brew install nmap              # Network scanner
brew install wireshark         # Packet analyzer
brew install mtr               # Network diagnostics

# System security
brew install lynis             # Security auditing
brew install osquery           # System instrumentation
brew install rkhunter          # Rootkit hunter

# Password/credential tools
brew install pwgen             # Password generator
brew install 1password-cli     # 1Password CLI

# Malware scanning
brew install clamav            # Antivirus

Running Lynis Security Audit

# Install Lynis
$ brew install lynis

# Run audit (comprehensive security scan)
$ sudo lynis audit system

# Quick audit
$ sudo lynis audit system --quick

# View report
$ cat /var/log/lynis-report.dat

Privacy Hardening

Disable Telemetry

#!/bin/bash
# disable-telemetry.sh - Reduce data sent to Apple

# Disable Siri analytics
defaults write com.apple.assistant.support "Siri Data Sharing Opt-In Status" -int 2

# Disable analytics sharing
defaults write com.apple.CrashReporter DialogType -string "none"

# Disable personalized ads
defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool false

# Disable location-based suggestions
defaults write com.apple.assistant.support "Allow Location Suggestions" -bool false

echo "Telemetry settings updated. Restart for full effect."

Secure DNS

#!/bin/bash
# Configure encrypted DNS (DNS over HTTPS)

# Option 1: Use a configuration profile (recommended)
# Create a .mobileconfig file with DNS settings

# Option 2: Use networksetup
# Set DNS servers (example: Cloudflare)
networksetup -setdnsservers Wi-Fi 1.1.1.1 1.0.0.1

# Verify
networksetup -getdnsservers Wi-Fi

# Flush DNS cache
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

Incident Response

Collecting Evidence

#!/bin/bash
# collect-evidence.sh - Collect system state for incident analysis

EVIDENCE_DIR="$HOME/evidence-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$EVIDENCE_DIR"

echo "Collecting system evidence..."

# System info
system_profiler > "$EVIDENCE_DIR/system_profiler.txt"

# Running processes
ps aux > "$EVIDENCE_DIR/processes.txt"

# Network connections
netstat -an > "$EVIDENCE_DIR/netstat.txt"
lsof -i > "$EVIDENCE_DIR/network_connections.txt"

# Open files
lsof > "$EVIDENCE_DIR/open_files.txt"

# Login history
last > "$EVIDENCE_DIR/last_logins.txt"

# Installed apps
ls -la /Applications > "$EVIDENCE_DIR/applications.txt"

# Launch items
ls -la /Library/LaunchDaemons > "$EVIDENCE_DIR/launch_daemons.txt"
ls -la /Library/LaunchAgents > "$EVIDENCE_DIR/launch_agents_system.txt"
ls -la ~/Library/LaunchAgents > "$EVIDENCE_DIR/launch_agents_user.txt"

# Recent logs
log show --last 1h > "$EVIDENCE_DIR/recent_logs.txt"

# Kernel extensions
kextstat > "$EVIDENCE_DIR/kexts.txt"

# Browser history (if accessible)
cp ~/Library/Safari/History.db "$EVIDENCE_DIR/" 2>/dev/null
cp ~/Library/Application\ Support/Google/Chrome/Default/History "$EVIDENCE_DIR/chrome_history" 2>/dev/null

# Create archive
tar -czf "$EVIDENCE_DIR.tar.gz" "$EVIDENCE_DIR"
rm -rf "$EVIDENCE_DIR"

echo "Evidence collected to $EVIDENCE_DIR.tar.gz"
echo "Preserve this file for analysis"

Malware Removal Checklist

#!/bin/bash
# malware-check.sh - Basic malware detection steps

echo "=== Malware Detection Checklist ==="

# 1. Check for suspicious launch items
echo "--- Checking Launch Items ---"
echo "System LaunchDaemons:"
ls /Library/LaunchDaemons/ | grep -v "com.apple"

echo "System LaunchAgents:"
ls /Library/LaunchAgents/ | grep -v "com.apple"

echo "User LaunchAgents:"
ls ~/Library/LaunchAgents/ 2>/dev/null

# 2. Check login items
echo
echo "--- Login Items ---"
osascript -e 'tell application "System Events" to get the name of every login item'

# 3. Check browser extensions
echo
echo "--- Browser Extensions ---"
ls ~/Library/Safari/Extensions/ 2>/dev/null
ls ~/Library/Application\ Support/Google/Chrome/Default/Extensions/ 2>/dev/null | head -10

# 4. Check for suspicious processes
echo
echo "--- Suspicious Processes ---"
ps aux | grep -E "^[^root]" | grep -v "^$USER" | grep -v "_"

# 5. Check network connections
echo
echo "--- Unusual Network Connections ---"
lsof -i | grep ESTABLISHED | grep -v -E "Safari|Chrome|firefox|Mail|Messages" | head -10

# 6. Check kernel extensions
echo
echo "--- Third-party Kernel Extensions ---"
kextstat | grep -v com.apple

# 7. Check profiles
echo
echo "--- Configuration Profiles ---"
profiles -P 2>/dev/null

echo
echo "=== Review output for suspicious items ==="
echo "If malware is found, consider professional help or factory reset"

Summary

Security best practices for macOS:

Essential Security

  1. Enable FileVault - Encrypt your disk
  2. Keep SIP enabled - Don’t disable without good reason
  3. Enable Gatekeeper - Block unsigned apps
  4. Turn on Firewall - Control network access
  5. Disable auto-login - Require authentication
  6. Enable automatic updates - Stay patched
  7. Use strong passwords - With a password manager
  8. Enable Find My Mac - For lost/stolen devices

Monitoring Checklist

  • Review security logs weekly
  • Audit TCC permissions monthly
  • Check for unsigned apps quarterly
  • Update security baseline after changes
  • Test backups regularly
  • Review user accounts quarterly

Quick Security Commands

# Check overall security posture
$ fdesetup status && csrutil status && spctl --status

# Review recent security events
$ log show --predicate 'subsystem == "com.apple.securityd"' --last 1h

# Audit network services
$ lsof -iTCP -sTCP:LISTEN -n -P

# Check for unsigned apps
$ for app in /Applications/*.app; do codesign -v "$app" 2>/dev/null || echo "Unsigned: $app"; done

# Reset suspicious app's permissions
$ tccutil reset All com.suspicious.app

Security is an ongoing process, not a one-time configuration. Regular audits, updates, and vigilance are essential for maintaining a secure macOS environment.

Cross-Platform Interoperability

Modern software development rarely happens on a single platform. Developers routinely build on macOS, deploy to Linux servers, share files with colleagues on different operating systems, and run containers that virtualize entirely different environments. macOS’s Unix heritage makes it an excellent development platform for cross-platform work, but its differences from Linux and BSD systems can cause friction if you don’t understand them.

This section provides practical guidance for developers who work across platforms, covering everything from file sharing quirks to running Linux containers natively on your Mac.

The Interoperability Challenge

macOS is Unix-certified and shares much with Linux and BSD, but important differences remain:

┌─────────────────────────────────────────────────────────────────┐
│                   Platform Comparison                            │
├─────────────────────┬──────────────────┬────────────────────────┤
│        macOS        │      Linux       │         BSD            │
├─────────────────────┼──────────────────┼────────────────────────┤
│ BSD userland        │ GNU userland     │ BSD userland           │
│ HFS+/APFS           │ ext4/xfs/btrfs   │ UFS/ZFS                │
│ launchd             │ systemd/init     │ rc.d                   │
│ Case-insensitive*   │ Case-sensitive   │ Case-sensitive         │
│ Extended attributes │ xattrs (diff.)   │ xattrs (diff.)         │
│ Frameworks          │ .so libraries    │ .so libraries          │
│ Mach-O binaries     │ ELF binaries     │ ELF binaries           │
│ Apple Silicon/Intel │ x86/ARM/etc.     │ x86/ARM/etc.           │
└─────────────────────┴──────────────────┴────────────────────────┘
* Default; case-sensitive option available

These differences affect:

  • File sharing: Case sensitivity, extended attributes, and line endings cause problems
  • Shell scripts: Command flags differ between BSD and GNU utilities
  • Containers: Linux containers need a VM layer on macOS
  • Remote work: SSH, VNC, and network protocols have platform-specific considerations

Common Cross-Platform Workflows

Development Workflow: Code on Mac, Deploy to Linux

The most common pattern for web developers:

┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│   macOS Local    │───▶│   Git/GitHub     │───▶│  Linux Server    │
│   Development    │    │                  │    │  (Production)    │
└──────────────────┘    └──────────────────┘    └──────────────────┘
        │                                               ▲
        │                                               │
        └───────────────────────────────────────────────┘
                    SSH / Container Testing

Key considerations:

  • Line endings (CRLF vs LF)
  • Case sensitivity in filenames
  • Shell script portability
  • Environment parity with containers

Container-Based Development

Running Linux containers on macOS:

┌─────────────────────────────────────────────────────────────────┐
│                         macOS Host                               │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              Linux VM (Docker/Colima/Lima)                 │  │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐       │  │
│  │  │ Container 1 │  │ Container 2 │  │ Container 3 │       │  │
│  │  │   (app)     │  │   (db)      │  │   (cache)   │       │  │
│  │  └─────────────┘  └─────────────┘  └─────────────┘       │  │
│  └───────────────────────────────────────────────────────────┘  │
│                              │                                   │
│                    Volume mounts (with translation)              │
│                              │                                   │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                    macOS Filesystem                        │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Multi-Machine Environment

Working with multiple systems:

# SSH config for quick access
$ cat ~/.ssh/config
Host dev
    HostName dev-server.example.com
    User developer

Host prod-*
    User deploy
    IdentityFile ~/.ssh/deploy_key
    ProxyJump bastion.example.com

# Quick connections
$ ssh dev
$ ssh prod-web-1

Quick Compatibility Reference

File Systems and Paths

IssuemacOSLinuxSolution
Case sensitivityInsensitiveSensitiveTest on Linux or case-sensitive volume
Path separator//Same
Home directory/Users/name/home/nameUse $HOME or ~
Temp directory/tmp (→ /private/tmp)/tmpUse $TMPDIR or /tmp
Extended attrsYes (complex)Yes (simpler)Strip with xattr -rc before sharing

Shell Commands

TaskmacOS (BSD)Linux (GNU)Portable
List fileslslsSame (basic flags)
Extended listls -l@ls -lCheck for attrs separately
sed in-placesed -i ''sed -ised -i.bak
Date formatBSD dateGNU dateUse date +%FORMAT
Find with deletefind -deletefind -deleteSame
readlinkBSD (limited)GNU (full)Install GNU coreutils
stat formatstat -fstat -cUse format flags carefully

Network Services

ServicemacOSLinuxNotes
SSHOpenSSHOpenSSHMostly identical
VNCScreen SharingTigerVNC/x11vncDifferent implementations
SMBBuilt-inSambamacOS client works with Linux servers
NFSBuilt-inBuilt-inSome option differences
mDNS/BonjourBuilt-inAvahiCompatible protocols

What You’ll Learn in This Part

File Sharing with Linux and BSD covers the pitfalls of moving files between systems, including case sensitivity, extended attributes, line endings, and the best portable formats like ExFAT.

Running Linux Containers on macOS explains how Docker Desktop, Colima, Podman, and Lima work, including the virtualization layer required to run Linux containers on macOS.

Virtual Machines on macOS covers running full Linux distributions on your Mac using UTM, Parallels, VMware, and Apple’s Virtualization.framework.

Cross-Platform Script Compatibility provides techniques for writing shell scripts that work on both macOS and Linux, covering POSIX compliance, shebang lines, OS detection, and portable patterns.

SSH Configuration and Usage dives deep into SSH configuration, key management, agent forwarding, ProxyJump, multiplexing, and advanced features for developers.

Remote Access: VNC and ARD covers accessing your Mac remotely and using your Mac to access other systems via VNC, Apple Remote Desktop, and other protocols.

Working with NFS and SMB explains mounting network shares, configuring autofs for automatic mounting, and troubleshooting common permission issues.

Quick Interoperability Checklist

Before sharing files or scripts across platforms:

# Check for case-sensitivity issues in a directory
find . -type f | sort -f | uniq -di

# Remove extended attributes before archiving
xattr -rc directory/

# Convert line endings to Unix format
find . -name "*.sh" -exec dos2unix {} \;
# Or using sed:
find . -name "*.sh" -exec sed -i '' 's/\r$//' {} \;

# Test script POSIX compliance
shellcheck --shell=sh script.sh

# Check shebang portability
head -1 script.sh
# Use: #!/usr/bin/env bash
# Not: #!/usr/local/bin/bash

The following chapters provide detailed guidance on each of these areas, helping you work seamlessly across macOS, Linux, and BSD systems.

File Sharing with Linux and BSD

Moving files between macOS and Linux or BSD systems seems straightforward until you encounter the edge cases: files that vanish because of case sensitivity, mysterious extended attributes polluting your archives, or scripts that fail because of invisible line ending differences. This chapter covers these common issues and provides solutions for reliable cross-platform file sharing.

Case Sensitivity: The Silent Problem

By default, macOS uses a case-insensitive (but case-preserving) filesystem. Linux and BSD use case-sensitive filesystems. This difference causes real problems.

The Problem

# On macOS (case-insensitive)
$ touch README.md readme.md
$ ls
README.md  # Only one file exists!

# On Linux (case-sensitive)
$ touch README.md readme.md
$ ls
README.md  readme.md  # Two distinct files

When you clone a repository on macOS that contains both File.txt and file.txt (created on Linux), one will silently overwrite the other.

Detecting Case Conflicts

Check for case conflicts before they cause problems:

# Find potential case conflicts in current directory
find . -type f | sort -f | uniq -di

# More comprehensive check (shows conflicting pairs)
find . -type f -print0 | \
  xargs -0 -n1 basename | \
  sort -f | uniq -di | while read name; do
    find . -iname "$name" -type f
    echo "---"
  done

# Check a git repository for case conflicts
git ls-files | sort -f | uniq -di

Solutions

Option 1: Create a Case-Sensitive Volume

For development that must match Linux behavior:

# Create a case-sensitive APFS volume
$ diskutil apfs addVolume disk1 "Case-sensitive APFS" CaseSensitive

# Mount point will be at /Volumes/CaseSensitive
$ cd /Volumes/CaseSensitive
$ touch README.md readme.md
$ ls
README.md  readme.md  # Both files exist

Option 2: Create a Case-Sensitive Disk Image

For a portable solution:

# Create a sparse image with case-sensitive filesystem
$ hdiutil create -size 10g -fs "Case-sensitive APFS" \
    -type SPARSE -volname "DevWork" ~/DevWork.sparseimage

# Mount it
$ hdiutil attach ~/DevWork.sparseimage
/dev/disk4          	GUID_partition_scheme
/dev/disk4s1        	EFI
/dev/disk4s2        	Apple_APFS
/dev/disk5          	EFI
/dev/disk5s1        	XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX	/Volumes/DevWork

# Use it
$ cd /Volumes/DevWork

Option 3: Git Configuration

Configure Git to warn about case issues:

# Enable case-sensitivity checking
$ git config core.ignoreCase false

# This will cause Git to notice case-only renames

Option 4: Rename Files to Avoid Conflicts

The simplest solution when possible:

# On Linux, rename conflicting files
$ mv file.txt file_lower.txt
$ mv File.txt File_upper.txt

Extended Attributes and Resource Forks

macOS extensively uses extended attributes for metadata. These cause problems when sharing files with Linux/BSD.

The Problem

# On macOS, files acquire extended attributes
$ ls -l@ file.txt
-rw-r--r--@ 1 user staff 100 Jan 1 12:00 file.txt
    com.apple.quarantine	     57
    com.apple.lastuseddate#PS	     16

# When archived, these create ._ files (AppleDouble format)
$ tar czf archive.tar.gz folder/
$ tar tzf archive.tar.gz | grep "._"
folder/._file.txt
folder/._image.png

On Linux, these ._ files litter your directories:

# On Linux after extracting
$ ls -la
-rw-r--r-- 1 user user  100 Jan  1 12:00 file.txt
-rw-r--r-- 1 user user  289 Jan  1 12:00 ._file.txt  # What is this?

Understanding Extended Attributes

View extended attributes on macOS:

# List extended attributes
$ xattr file.txt
com.apple.quarantine
com.apple.lastuseddate#PS

# View attribute contents
$ xattr -p com.apple.quarantine file.txt
0083;5f4c1234;Safari;1234ABCD-5678-EFGH-IJKL-MNOPQRSTUVWX

# Common macOS extended attributes
# com.apple.quarantine     - File downloaded from internet
# com.apple.FinderInfo     - Finder metadata
# com.apple.ResourceFork   - Classic Mac resource data
# com.apple.lastuseddate#PS - Last opened date
# com.apple.metadata:*     - Spotlight metadata

Removing Extended Attributes

Before sharing files with other systems:

# Remove all extended attributes from a file
$ xattr -c file.txt

# Remove all extended attributes recursively
$ xattr -rc folder/

# Remove specific attribute
$ xattr -d com.apple.quarantine file.txt

# Remove quarantine attribute from downloaded app
$ xattr -dr com.apple.quarantine /Applications/SomeApp.app

Creating Clean Archives

For archives meant for Linux/BSD systems:

# Method 1: Use tar with --no-xattrs (if available)
$ tar --no-xattrs -czf archive.tar.gz folder/

# Method 2: Strip attributes first
$ xattr -rc folder/
$ tar czf archive.tar.gz folder/

# Method 3: Use COPYFILE_DISABLE to prevent AppleDouble files
$ COPYFILE_DISABLE=1 tar czf archive.tar.gz folder/

# Method 4: For zip files
$ zip -r -X archive.zip folder/  # -X excludes extra file attributes

# Verify archive is clean
$ tar tzf archive.tar.gz | grep "._"
# Should return nothing

Cleaning Received Archives

When you receive archives with ._ files:

# Remove AppleDouble files
$ find . -name "._*" -delete

# Alternative: use dot_clean utility
$ dot_clean folder/
# This merges ._ files back into their parent files or removes them

Line Endings: The Invisible Difference

Different operating systems use different line endings:

  • Unix/Linux/macOS: LF (\n, 0x0A)
  • Windows: CRLF (\r\n, 0x0D 0x0A)
  • Classic Mac (pre-OS X): CR (\r, 0x0D)

Detecting Line Ending Issues

# Check file line endings
$ file script.sh
script.sh: Bourne-Again shell script, ASCII text executable

$ file windows_script.sh
windows_script.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators

# Using cat -v to see carriage returns
$ cat -v windows_script.sh | head -2
#!/bin/bash^M
echo "Hello"^M

# Using hexdump
$ hexdump -C script.sh | head -2
00000000  23 21 2f 62 69 6e 2f 62  61 73 68 0a              |#!/bin/bash.|

Converting Line Endings

# Using dos2unix (install via Homebrew)
$ brew install dos2unix

# Convert Windows to Unix
$ dos2unix file.txt

# Convert Unix to Windows
$ unix2dos file.txt

# Convert multiple files
$ dos2unix *.sh

# Without dos2unix, use sed
# Remove CR characters (Windows → Unix)
$ sed -i '' 's/\r$//' file.txt

# Using tr
$ tr -d '\r' < windows_file.txt > unix_file.txt

# Using Perl (often more reliable for binary safety)
$ perl -pi -e 's/\r\n/\n/g' file.txt

Git Configuration for Line Endings

Configure Git to handle line endings automatically:

# For cross-platform projects (recommended)
# This normalizes to LF in repository, converts to native on checkout
$ git config core.autocrlf input   # On macOS/Linux
$ git config core.autocrlf true    # On Windows

# Or use .gitattributes in the repository (better):
$ cat .gitattributes
# Set default behavior
* text=auto

# Explicitly declare text files
*.sh text eol=lf
*.py text eol=lf
*.md text eol=lf

# Declare binary files
*.png binary
*.jpg binary
*.pdf binary

# Windows batch files need CRLF
*.bat text eol=crlf

Fixing Line Endings in a Git Repository

# Normalize all text files in repository
$ git add --renormalize .
$ git commit -m "Normalize line endings"

ExFAT: The Universal Filesystem

When you need to share files via USB drives or SD cards, ExFAT is the best choice for cross-platform compatibility.

Why ExFAT?

FilesystemmacOSLinuxWindowsMax File Size
HFS+R/WRead*No8 EB
APFSR/WRead*No8 EB
NTFSReadR/WR/W16 TB
FAT32R/WR/WR/W4 GB
ExFATR/WR/WR/W16 EB

*With third-party tools

ExFAT is:

  • Native read/write on all three platforms
  • No 4GB file size limit (unlike FAT32)
  • Good for large external drives

Formatting as ExFAT

# Find disk identifier
$ diskutil list
/dev/disk4 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *32.0 GB    disk4
   1:             Windows_FAT_32 UNTITLED                32.0 GB    disk4s1

# Format as ExFAT (WARNING: erases all data)
$ diskutil eraseDisk ExFAT SharedDrive /dev/disk4
Started erase on disk4
Unmounting disk
Creating the partition map
Waiting for partitions to activate
Formatting disk4s2 as ExFAT with name SharedDrive
Volume name      : SharedDrive
Partition offset : 411648 sectors (210763776 bytes)
Volume size      : 62078976 sectors (31784435712 bytes)
Bytes per sector : 512
Bytes per cluster: 32768
FAT offset       : 2048 sectors (1048576 bytes)
# FAT sectors    : 7680
Number of FATs   : 1
Cluster offset   : 9728 sectors (4980736 bytes)
# Clusters       : 969666
Volume Serial Number: XXXX-XXXX
Mounting disk
Finished erase on disk4

ExFAT Limitations on macOS

ExFAT on macOS has some restrictions:

# No extended attributes support
$ xattr -w user.test "value" /Volumes/SharedDrive/file.txt
xattr: file.txt: No such xattr: user.test

# No symbolic links
$ ln -s file.txt /Volumes/SharedDrive/link.txt
ln: /Volumes/SharedDrive/link.txt: Function not implemented

# No hard links
$ ln file.txt /Volumes/SharedDrive/hardlink.txt
ln: /Volumes/SharedDrive/hardlink.txt: Operation not supported

Network Share Considerations

When sharing files over the network, format differences still matter.

SMB Shares from Linux

When accessing Linux SMB (Samba) shares from macOS:

# Mount with specific options
$ mount_smbfs //user@server/share /mnt/share

# For better compatibility with Linux servers
$ mount_smbfs -o nounix //user@server/share /mnt/share

Common issues:

  • Permission mapping: Unix permissions don’t map perfectly to SMB
  • Case sensitivity: Linux share is case-sensitive; macOS client may cache incorrectly
  • Extended attributes: May not be preserved across SMB

NFS Shares

NFS is often more Unix-friendly:

# Mount NFS share
$ sudo mount -t nfs server:/export/share /mnt/share

# With specific options for Linux NFS servers
$ sudo mount -t nfs -o vers=3,resvport server:/export/share /mnt/share

See the Working with NFS and SMB chapter for detailed coverage.

Archive Formats for Cross-Platform Sharing

# Clean, cross-platform archive
$ xattr -rc folder/
$ COPYFILE_DISABLE=1 tar czf archive.tar.gz folder/

Alternative: zip Without macOS Extras

# Create clean zip
$ zip -r -X archive.zip folder/

# Exclude macOS metadata files
$ zip -r archive.zip folder/ -x "*.DS_Store" -x "*__MACOSX*" -x "*._*"

Receiving Archives from Other Systems

# Linux tar archives work fine on macOS
$ tar xzf linux-archive.tar.gz

# If you encounter extended attribute errors
$ tar xzf archive.tar.gz --no-xattrs 2>/dev/null

Practical Workflow: Syncing Project with Linux Server

A complete workflow for keeping a project in sync:

#!/bin/bash
# sync-to-server.sh - Sync project to Linux server cleanly

PROJECT_DIR="$HOME/projects/myapp"
REMOTE_HOST="dev@linux-server"
REMOTE_DIR="/home/dev/projects/myapp"

# Remove extended attributes
xattr -rc "$PROJECT_DIR"

# Sync with rsync, excluding macOS-specific files
rsync -avz --delete \
    --exclude='.DS_Store' \
    --exclude='._*' \
    --exclude='.AppleDouble' \
    --exclude='__MACOSX' \
    --exclude='.Spotlight-*' \
    --exclude='.Trashes' \
    --exclude='.fseventsd' \
    "$PROJECT_DIR/" \
    "$REMOTE_HOST:$REMOTE_DIR/"

Global Git Configuration

Configure Git globally for cross-platform work:

# ~/.gitconfig additions
$ cat >> ~/.gitconfig << 'EOF'

[core]
    # Handle line endings
    autocrlf = input

    # Warn about case issues
    ignoreCase = false

    # Ignore extended attribute changes
    ignoreStat = false

[transfer]
    # Stricter checking
    fsckObjects = true
EOF

Summary

IssueDetectionSolution
Case sensitivityfind . | sort -f | uniq -diUse case-sensitive volume or rename files
Extended attributesls -l@, xattr -lxattr -rc before sharing
AppleDouble filesfind . -name "._*"COPYFILE_DISABLE=1 or dot_clean
Line endingsfile command, cat -vdos2unix or configure .gitattributes
External drivesN/AFormat as ExFAT for universal compatibility

Key practices:

  1. Clean files before sharing: xattr -rc folder/
  2. Use clean archive creation: COPYFILE_DISABLE=1 tar czf
  3. Configure Git properly: .gitattributes with explicit line ending rules
  4. Test on target platform: Especially for case sensitivity issues
  5. Use ExFAT for shared drives: Works everywhere without drivers

Running Linux Containers on macOS

Containers have revolutionized software development, but there’s a fundamental challenge on macOS: containers are a Linux technology. Features like cgroups, namespaces, and overlay filesystems are Linux kernel features that don’t exist in macOS’s XNU kernel. Running Linux containers on macOS requires virtualization to provide a Linux kernel. This chapter explains how different container solutions accomplish this and helps you choose the right one for your workflow.

How Containers Work on macOS

On Linux, containers run directly on the host kernel:

┌─────────────────────────────────────────────────┐
│                 Linux Host                       │
│  ┌───────────┐ ┌───────────┐ ┌───────────┐     │
│  │Container 1│ │Container 2│ │Container 3│     │
│  └─────┬─────┘ └─────┬─────┘ └─────┬─────┘     │
│        │             │             │            │
│  ┌─────┴─────────────┴─────────────┴─────┐     │
│  │          Linux Kernel                  │     │
│  │    (cgroups, namespaces, overlayfs)   │     │
│  └────────────────────────────────────────┘     │
└─────────────────────────────────────────────────┘

On macOS, a Linux VM sits between containers and the host:

┌─────────────────────────────────────────────────┐
│                  macOS Host                      │
│  ┌───────────────────────────────────────────┐  │
│  │              Linux VM                      │  │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐     │  │
│  │  │Container│ │Container│ │Container│     │  │
│  │  └────┬────┘ └────┬────┘ └────┬────┘     │  │
│  │       │           │           │          │  │
│  │  ┌────┴───────────┴───────────┴────┐     │  │
│  │  │        Linux Kernel              │     │  │
│  │  └──────────────────────────────────┘     │  │
│  └───────────────────────────────────────────┘  │
│                       │                          │
│           Hypervisor / Virtualization            │
│  ┌───────────────────────────────────────────┐  │
│  │     Virtualization.framework / QEMU        │  │
│  └───────────────────────────────────────────┘  │
│                       │                          │
│  ┌───────────────────────────────────────────┐  │
│  │              XNU Kernel                    │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘

This architecture has implications for performance, file sharing, and networking.

Virtualization Technologies

Apple Virtualization.framework

Apple’s native virtualization framework, introduced in macOS Big Sur and significantly enhanced for Apple Silicon:

  • Apple Silicon: Near-native performance, runs ARM64 Linux
  • Intel: Uses hardware virtualization (VT-x)
  • Features: Fast boot, file sharing, networking, Rosetta 2 for x86 emulation

Used by: Docker Desktop, Colima, Lima

QEMU

Open-source machine emulator and virtualizer:

  • Emulation: Can run x86 Linux on ARM Mac (slower)
  • Virtualization: Fast when using HVF (Hypervisor.framework) or Virtualization.framework
  • Flexibility: Supports many architectures

Used by: UTM, Lima (optional), Podman Machine

Hypervisor.framework

Apple’s lower-level virtualization API (Intel only):

  • Used by older container solutions on Intel Macs
  • More direct hardware access
  • Being superseded by Virtualization.framework

Docker Desktop

Docker Desktop is the official Docker solution for macOS. It provides a complete container development environment.

Installation

# Via Homebrew Cask
$ brew install --cask docker

# Or download from docker.com

Architecture

┌────────────────────────────────────────────────┐
│                Docker Desktop                   │
│  ┌──────────────────────────────────────────┐  │
│  │            GUI / Menu Bar App             │  │
│  └──────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────┐  │
│  │        Docker Engine (in Linux VM)        │  │
│  │  ┌──────────────────────────────────────┐│  │
│  │  │   containerd → containers            ││  │
│  │  └──────────────────────────────────────┘│  │
│  └──────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────┐  │
│  │    Linux VM (custom LinuxKit distro)     │  │
│  └──────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────┐  │
│  │    Virtualization.framework / HVF        │  │
│  └──────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘

Configuration

Docker Desktop settings (accessible via GUI or ~/.docker/config.json):

# View current configuration
$ cat ~/.docker/config.json

# Resource allocation (GUI or settings file)
# CPUs: Number of CPU cores for the VM
# Memory: RAM allocation (default: 2GB)
# Disk: Virtual disk size

# Example config.json
{
  "auths": {},
  "credHelpers": {
    "docker.io": "desktop"
  },
  "currentContext": "desktop-linux"
}

File Sharing

Docker Desktop provides multiple file sharing mechanisms:

# Volume mounts use gRPC FUSE (default) or VirtioFS
# Check current mechanism in Docker Desktop settings

# Mount a local directory
$ docker run -v ~/Projects/myapp:/app -it ubuntu bash

# VirtioFS (faster, macOS 12.5+)
# Enable in: Docker Desktop → Settings → General → VirtioFS

Performance tip: Use .dockerignore to exclude files that don’t need to be in the build context:

# .dockerignore
node_modules
.git
*.log
.DS_Store

Networking

# Host networking mode isn't fully supported
# Containers use bridge networking by default

# Publish ports to macOS host
$ docker run -p 8080:80 nginx

# Access from Mac at localhost:8080

# Container-to-container networking
$ docker network create mynet
$ docker run --network mynet --name db postgres
$ docker run --network mynet myapp  # Can reach db by hostname

Docker Compose

# Install (included with Docker Desktop)
$ docker compose version

# Example compose file
$ cat docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
    depends_on:
      - db
  db:
    image: postgres:14
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

# Start services
$ docker compose up -d

Pros and Cons

Pros:

  • Official Docker product, best documentation
  • Integrated Kubernetes
  • GUI for management
  • Automatic updates

Cons:

  • Resource intensive (runs constantly in background)
  • Commercial license required for larger companies
  • Can be slow for file-heavy operations

Colima: Lightweight Docker Alternative

Colima provides Docker and containerd runtimes in a lightweight VM, using Lima under the hood.

Installation

# Install Colima and Docker CLI
$ brew install colima docker docker-compose

# Start Colima (creates and starts VM)
$ colima start

# Verify Docker works
$ docker ps

Architecture

┌────────────────────────────────────────────────┐
│                   Colima                        │
│  ┌──────────────────────────────────────────┐  │
│  │              Lima VM                      │  │
│  │  ┌──────────────────────────────────────┐│  │
│  │  │    Docker daemon / containerd        ││  │
│  │  └──────────────────────────────────────┘│  │
│  │  ┌──────────────────────────────────────┐│  │
│  │  │    Alpine Linux (minimal)            ││  │
│  │  └──────────────────────────────────────┘│  │
│  └──────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────┐  │
│  │       Virtualization.framework           │  │
│  └──────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘

Configuration

# Start with custom resources
$ colima start --cpu 4 --memory 8 --disk 60

# View current configuration
$ colima status

# Use specific runtime
$ colima start --runtime docker     # Docker (default)
$ colima start --runtime containerd # containerd + nerdctl

# Use Rosetta 2 for x86 emulation (Apple Silicon)
$ colima start --arch x86_64 --vm-type vz --vz-rosetta

# Multiple profiles for different projects
$ colima start --profile work --cpu 4 --memory 8
$ colima start --profile personal --cpu 2 --memory 4

# Switch between profiles
$ colima list
$ docker context use colima-work

File Sharing

# By default, mounts home directory
$ colima start  # Mounts ~ into VM

# Custom mounts
$ colima start --mount ~/Projects:w  # Read-write mount

# VirtioFS (faster, Apple Silicon + macOS 13+)
$ colima start --vm-type vz --mount-type virtiofs

Managing Colima

# Start/stop
$ colima start
$ colima stop

# SSH into the VM
$ colima ssh

# Delete VM (removes all containers and images)
$ colima delete

# Update Colima itself
$ brew upgrade colima

Pros and Cons

Pros:

  • Lightweight, starts quickly
  • Free and open source
  • Uses Docker CLI (drop-in replacement)
  • Multiple profiles for different resource needs

Cons:

  • Less polished than Docker Desktop
  • No built-in Kubernetes (but easy to add)
  • Community supported

Podman: Daemonless Containers

Podman runs containers without a central daemon, providing a more Unix-like approach. Each container is a child process of the podman command.

Installation

# Install Podman
$ brew install podman

# Initialize the Podman machine (creates VM)
$ podman machine init

# Start the machine
$ podman machine start

# Verify it's running
$ podman info

Architecture

┌────────────────────────────────────────────────┐
│                   Podman                        │
│  ┌──────────────────────────────────────────┐  │
│  │           Podman Machine                  │  │
│  │  ┌──────────────────────────────────────┐│  │
│  │  │   podman (runs containers directly)  ││  │
│  │  └──────────────────────────────────────┘│  │
│  │  ┌──────────────────────────────────────┐│  │
│  │  │         Fedora CoreOS                ││  │
│  │  └──────────────────────────────────────┘│  │
│  └──────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────┐  │
│  │              QEMU + HVF                  │  │
│  └──────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘

Docker Compatibility

Podman is CLI-compatible with Docker:

# Most Docker commands work identically
$ podman pull nginx
$ podman run -d -p 8080:80 nginx
$ podman ps
$ podman stop <container>

# Create alias for complete compatibility
$ alias docker=podman

# Or use the podman-docker package
$ brew install podman-docker
# Creates /usr/local/bin/docker → podman

Podman Compose

# Install podman-compose
$ pip3 install podman-compose

# Or use Docker Compose with Podman
$ podman compose up -d  # Works with docker-compose.yml

Rootless Containers

Podman’s design supports rootless containers:

# Run container as non-root (default on macOS)
$ podman run -it alpine whoami
root  # Root inside container, but mapped to user outside

Managing Podman Machines

# List machines
$ podman machine list

# Stop machine
$ podman machine stop

# SSH into machine
$ podman machine ssh

# Configure machine resources
$ podman machine init --cpus 4 --memory 4096 --disk-size 50

# Remove machine
$ podman machine rm

Pros and Cons

Pros:

  • Daemonless architecture
  • Docker CLI compatible
  • Rootless by default
  • Podman Desktop GUI available

Cons:

  • Fewer features than Docker Desktop
  • Some Docker Compose edge cases may not work
  • Smaller community

Lima: Linux Machines for macOS

Lima creates Linux virtual machines with automatic file sharing and port forwarding. It’s the foundation for Colima but can be used directly for running Linux VMs.

Installation

$ brew install lima

Creating a VM

# Create default VM (Ubuntu)
$ limactl create default

# Start VM
$ limactl start default

# Run command in VM
$ lima uname -a
Linux lima-default 5.15.0-xx-generic ...

# Get a shell in VM
$ lima
user@lima-default:~$

# Run Linux binaries directly
$ lima apt update
$ lima docker run hello-world

Pre-configured Templates

Lima includes templates for common use cases:

# List available templates
$ limactl create --list-templates

# Docker template
$ limactl create --name docker template://docker
$ limactl start docker

# Kubernetes (k3s)
$ limactl create --name k3s template://k3s
$ limactl start k3s

# Fedora
$ limactl create --name fedora template://fedora

# Arch Linux
$ limactl create --name arch template://archlinux

Custom Configuration

# my-linux.yaml
images:
  - location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
    arch: "x86_64"
  - location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-arm64.img"
    arch: "aarch64"

cpus: 4
memory: "8GiB"
disk: "50GiB"

mounts:
  - location: "~/Projects"
    writable: true

containerd:
  system: true
  user: false

provision:
  - mode: system
    script: |
      #!/bin/bash
      apt-get update
      apt-get install -y build-essential git

portForwards:
  - guestPort: 3000
    hostPort: 3000
# Create VM from custom config
$ limactl create --name myvm my-linux.yaml
$ limactl start myvm

File Sharing

Lima provides automatic file sharing:

# Default: home directory is mounted read-only
# /tmp/lima is mounted read-write

# In VM, macOS home is at:
$ lima ls /Users/yourname/

# Writable mount configuration in yaml:
mounts:
  - location: "~/Projects"
    writable: true

Pros and Cons

Pros:

  • Very flexible, runs full Linux distributions
  • Lightweight VMs
  • Good file sharing
  • Foundation for other tools

Cons:

  • More manual setup
  • Not specifically for containers (though supports them)
  • Less integrated container workflow

Comparison Matrix

FeatureDocker DesktopColimaPodmanLima
Setup ComplexityEasyEasyMediumMedium
Resource UsageHighLowMediumLow
Docker CLINativeNativeCompatibleManual
GUIYesNoOptionalNo
KubernetesBuilt-inAdd-onOptionalTemplates
LicenseCommercial*FreeFreeFree
File PerformanceGoodGoodAdequateGood
Apple SiliconYesYesYesYes
x86 EmulationYesVia RosettaVia QEMUVia QEMU

*Free for personal use and small businesses

Performance Optimization

General Tips

# Use volume mounts sparingly for large directories
# Instead, use named volumes for node_modules, vendor, etc.

# docker-compose.yml
services:
  app:
    volumes:
      - .:/app
      - node_modules:/app/node_modules  # Named volume, not bind mount

volumes:
  node_modules:

VirtioFS for Better File Performance

# Docker Desktop: Enable in Settings → General → VirtioFS

# Colima: Use VZ runtime with VirtioFS
$ colima start --vm-type vz --mount-type virtiofs

Multi-Architecture Builds

For building images that work on both ARM and x86:

# Enable buildx (Docker Desktop has this by default)
$ docker buildx create --use

# Build for multiple platforms
$ docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

# Build and push to registry
$ docker buildx build --platform linux/amd64,linux/arm64 \
    -t registry/myapp:latest --push .

Rosetta 2 for x86 Containers

Apple Silicon can run x86 Linux binaries via Rosetta 2:

# Colima with Rosetta
$ colima start --arch x86_64 --vm-type vz --vz-rosetta

# Docker Desktop: Enable in Settings → Features in development → Rosetta

# Now x86 images run with Rosetta translation
$ docker run --platform linux/amd64 ubuntu uname -m
x86_64

Quick Start Recommendations

For Docker Desktop users wanting a free alternative:

$ brew install colima docker
$ colima start
# Use docker as normal

For minimal resource usage:

$ brew install colima docker
$ colima start --cpu 2 --memory 4 --disk 30
$ colima stop  # When not in use

For full Linux development environment:

$ brew install lima
$ limactl create --name dev template://ubuntu
$ limactl start dev
$ lima  # Enter Linux shell

For rootless/daemonless preference:

$ brew install podman
$ podman machine init
$ podman machine start

Summary

All container solutions on macOS require a Linux VM layer. The choice depends on your needs:

  • Docker Desktop: Best for teams standardized on Docker, need Kubernetes, want GUI
  • Colima: Best for Docker users wanting lightweight, free alternative
  • Podman: Best for those preferring daemonless architecture, OCI standards focus
  • Lima: Best for running full Linux VMs, maximum flexibility

Key points:

  1. Containers on macOS always run in a Linux VM
  2. Apple’s Virtualization.framework provides near-native performance
  3. File sharing between macOS and containers has performance overhead
  4. VirtioFS significantly improves file sharing performance
  5. Rosetta 2 enables x86 container support on Apple Silicon

Virtual Machines on macOS

While containers provide lightweight isolation for running Linux applications, virtual machines offer complete operating system virtualization. This is essential when you need to run a full Linux desktop environment, test installers, run Windows, or develop for architectures that differ from your host. macOS supports virtualization through multiple solutions, from Apple’s native Virtualization.framework to established products like Parallels and VMware.

Virtualization Technologies on macOS

Apple Virtualization.framework

Apple’s native hypervisor, introduced in macOS Big Sur (11.0) and significantly enhanced in Monterey (12.0) and later:

┌─────────────────────────────────────────────────┐
│                  macOS Host                      │
│  ┌───────────────────────────────────────────┐  │
│  │            Guest VM (Linux/macOS)          │  │
│  │  ┌─────────────────────────────────────┐  │  │
│  │  │         Virtualized Hardware         │  │  │
│  │  │   CPU, Memory, Disk, Network, GPU    │  │  │
│  │  └─────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────┐  │
│  │       Virtualization.framework            │  │
│  │  (Native Apple hypervisor technology)     │  │
│  └───────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────┐  │
│  │              XNU Kernel                   │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘

Features:

  • Near-native performance on Apple Silicon
  • Low latency, efficient memory sharing
  • Rosetta 2 support for running x86 Linux on ARM
  • VirtioFS for fast file sharing
  • GPU acceleration (limited)

Limitations:

  • macOS guests only on Apple Silicon
  • No Windows guests (Apple Silicon)
  • Limited legacy hardware emulation

Hypervisor.framework

The lower-level hypervisor API, primarily for Intel Macs:

  • Direct CPU virtualization (VT-x)
  • Used by Docker Desktop (Intel), VMware, Parallels
  • Being superseded by Virtualization.framework

QEMU

Open-source machine emulator:

  • Emulation mode: Run any architecture (slow)
  • Virtualization mode: Near-native speed when combined with Apple’s hypervisors
  • Can emulate x86 on ARM Mac (and vice versa)

UTM: Free and Open Source

UTM is a full-featured virtualization app built on QEMU with a native macOS interface.

Installation

# Via Homebrew
$ brew install --cask utm

# Or download from https://getutm.app

Creating a Linux VM

  1. Download an ISO: Get a Linux distribution ISO (e.g., Ubuntu, Fedora)

  2. Create new VM in UTM:

    • Click “Create a New Virtual Machine”
    • Select “Virtualize” (for ARM Linux on Apple Silicon)
    • Select “Emulate” (for x86 Linux on Apple Silicon, slower)
    • Select Linux as the operating system
    • Browse to your ISO file
  3. Configure Resources:

    • Memory: 4GB+ recommended
    • CPU Cores: 2-4 for good performance
    • Storage: 30GB+ for development

Using UTM from Terminal

UTM supports automation via AppleScript and the utmctl command:

# List VMs
$ utmctl list

# Start a VM
$ utmctl start "Ubuntu 24.04"

# Stop a VM
$ utmctl stop "Ubuntu 24.04"

# Get VM IP address (for SSH)
$ utmctl ip "Ubuntu 24.04"
192.168.64.5

UTM Virtualization vs Emulation

ModeUse CasePerformance
VirtualizeARM Linux on Apple SiliconNear-native
Virtualizex86 Linux on Intel MacNear-native
Emulatex86 Linux on Apple SiliconSlow (~10% native)
EmulateARM Linux on Intel MacSlow

Shared Folders

UTM supports shared folders via SPICE WebDAV or VirtioFS:

# In guest Linux, mount VirtioFS share (if configured)
$ sudo mkdir /mnt/share
$ sudo mount -t virtiofs share /mnt/share

# Or use WebDAV mount
$ sudo mount -t davfs http://localhost:9843 /mnt/share

Pros and Cons

Pros:

  • Free and open source
  • Native macOS app with good UI
  • Supports both virtualization and emulation
  • Can run Windows, Linux, other OSes

Cons:

  • Emulation is slow for x86 on ARM
  • No GPU passthrough
  • Snapshots less seamless than commercial options

Parallels Desktop

Parallels is a commercial virtualization solution known for excellent performance and macOS integration.

Installation

# Via Homebrew
$ brew install --cask parallels

# Or download from https://www.parallels.com

Key Features

  • Coherence Mode: Run Linux apps as if they were macOS apps
  • Automatic optimization: Adjusts VM settings for best performance
  • Snapshots: Create and manage VM states
  • Rosetta 2 Linux: Run x86 binaries in ARM Linux guests

Creating a Linux VM

Parallels automates much of the setup:

# Command-line VM creation
$ prlctl create "Ubuntu Dev" --distribution ubuntu
$ prlctl set "Ubuntu Dev" --cpus 4 --memsize 8192
$ prlctl start "Ubuntu Dev"

Or through the GUI:

  1. File → New
  2. Download or select Linux ISO
  3. Configure resources
  4. Install

Parallels Command-Line Tools

# List VMs
$ prlctl list -a
UUID                                    STATUS       IP_ADDR         NAME
{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}  running      10.211.55.3     Ubuntu Dev

# Start/stop/suspend
$ prlctl start "Ubuntu Dev"
$ prlctl stop "Ubuntu Dev"
$ prlctl suspend "Ubuntu Dev"

# Create snapshot
$ prlctl snapshot "Ubuntu Dev" --name "Clean Install"

# Restore snapshot
$ prlctl snapshot-switch "Ubuntu Dev" --id "snap-id"

# SSH into VM
$ ssh user@$(prlctl list -o ip "Ubuntu Dev" 2>/dev/null | tail -1)

Shared Folders

# Configure shared folder
$ prlctl set "Ubuntu Dev" --shf-host on
$ prlctl set "Ubuntu Dev" --shared-folder-add shared --path ~/Shared

# In Linux guest, access at:
# /media/psf/shared

Networking Modes

# Shared networking (NAT) - default
$ prlctl set "Ubuntu Dev" --device-set net0 --type shared

# Bridged networking (VM on same network as host)
$ prlctl set "Ubuntu Dev" --device-set net0 --type bridged

# Host-only networking
$ prlctl set "Ubuntu Dev" --device-set net0 --type host-only

Pros and Cons

Pros:

  • Excellent performance, especially on Apple Silicon
  • Best Windows support on Apple Silicon
  • Great macOS integration (Coherence mode)
  • Commercial support

Cons:

  • Commercial license required (subscription model)
  • Can be expensive for occasional use
  • Runs background services

VMware Fusion

VMware Fusion is another commercial virtualization option with enterprise features.

Installation

# Via Homebrew
$ brew install --cask vmware-fusion

# Download from https://www.vmware.com/products/fusion.html
# Free "Fusion Player" license available for personal use

Creating a Linux VM

# Using vmrun command-line tool
$ vmrun -T fusion start /path/to/vm.vmx

# Or through GUI:
# File → New → Create Custom VM → Linux

VMware Command-Line Interface

# vmrun is the primary CLI tool
$ vmrun list  # List running VMs

# Start VM
$ vmrun -T fusion start "/path/to/vm.vmx"

# Stop VM
$ vmrun -T fusion stop "/path/to/vm.vmx"

# Soft stop (guest shutdown)
$ vmrun -T fusion stop "/path/to/vm.vmx" soft

# Suspend
$ vmrun -T fusion suspend "/path/to/vm.vmx"

# Take snapshot
$ vmrun -T fusion snapshot "/path/to/vm.vmx" "Snapshot Name"

# Revert to snapshot
$ vmrun -T fusion revertToSnapshot "/path/to/vm.vmx" "Snapshot Name"

# Run command in guest
$ vmrun -T fusion -gu username -gp password \
    runProgramInGuest "/path/to/vm.vmx" /bin/ls

# Copy file to guest
$ vmrun -T fusion -gu username -gp password \
    copyFileFromHostToGuest "/path/to/vm.vmx" \
    ~/local/file.txt /home/user/file.txt

Shared Folders

VMware Fusion uses HGFS (Host-Guest File System):

# In Linux guest, install open-vm-tools
$ sudo apt install open-vm-tools open-vm-tools-desktop

# Mount shared folder
$ sudo mkdir /mnt/hgfs
$ sudo mount -t fuse.vmhgfs-fuse .host:/ /mnt/hgfs -o allow_other

# Add to /etc/fstab for automatic mounting
# .host:/    /mnt/hgfs    fuse.vmhgfs-fuse    allow_other    0    0

Pros and Cons

Pros:

  • Enterprise-grade virtualization
  • Good Linux guest tools
  • Free personal use license
  • Established product

Cons:

  • Apple Silicon support still evolving
  • Heavier than UTM
  • Can conflict with other hypervisors

Using Virtualization.framework Directly

For developers wanting programmatic control, Apple’s Virtualization.framework can be used directly.

Swift Example

import Virtualization

// Create VM configuration
let config = VZVirtualMachineConfiguration()

// CPU
config.cpuCount = 4

// Memory
config.memorySize = 4 * 1024 * 1024 * 1024  // 4GB

// Boot loader (Linux)
let bootLoader = VZLinuxBootLoader(kernelURL: kernelURL)
bootLoader.initialRamdiskURL = initrdURL
bootLoader.commandLine = "console=hvc0"
config.bootLoader = bootLoader

// Storage
let diskAttachment = try VZDiskImageStorageDeviceAttachment(
    url: diskURL,
    readOnly: false
)
config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: diskAttachment)]

// Network
let network = VZNATNetworkDeviceAttachment()
config.networkDevices = [VZVirtioNetworkDeviceConfiguration(attachment: network)]

// Create and start VM
let vm = VZVirtualMachine(configuration: config)
try await vm.start()

Projects Using Virtualization.framework

Several open-source projects wrap Virtualization.framework:

  • Tart: VM management focused on macOS guests for CI
  • VirtualBuddy: GUI for macOS VMs
  • Lima: Configurable Linux VMs (covered in containers chapter)
# Tart for macOS VMs (good for CI)
$ brew install cirruslabs/cli/tart
$ tart clone ghcr.io/cirruslabs/macos-ventura-base:latest ventura-dev
$ tart run ventura-dev

Running Linux Distributions

Ubuntu

Ubuntu provides official ARM64 images for Apple Silicon:

# Download Ubuntu Server for ARM
# https://ubuntu.com/download/server/arm

# For desktop, use Ubuntu Desktop ARM64
# https://cdimage.ubuntu.com/jammy/daily-live/current/

Fedora

Fedora has excellent ARM64 support:

# Download Fedora Server/Workstation for aarch64
# https://getfedora.org/

# Fedora runs very well on Apple Silicon

Debian

Debian supports ARM64:

# https://www.debian.org/distrib/
# Download arm64 netinst or DVD image

Arch Linux

Arch Linux ARM for Apple Silicon:

# https://archlinuxarm.org/
# Requires more manual setup but works well

x86 Linux on Apple Silicon

Running x86 Linux on Apple Silicon Macs is possible but involves trade-offs.

Emulation (QEMU/UTM)

Full emulation is slow but compatible:

# In UTM, select "Emulate" instead of "Virtualize"
# Performance: ~10% of native
# Use case: Testing x86-specific software

Rosetta 2 Translation

Better option: ARM Linux with Rosetta for x86 binaries:

# In an ARM Linux VM (Parallels, UTM, Lima)
# Install Rosetta (if supported)

# On Parallels:
# Rosetta is auto-configured for supported distros

# On Lima with Rosetta:
$ limactl create --name rosetta template://default --rosetta

# Now x86 binaries run with Rosetta translation
$ lima
user@lima-rosetta:~$ arch
aarch64
user@lima-rosetta:~$ /usr/bin/x86_binary  # Runs via Rosetta

Rosetta in Linux provides much better performance than full emulation (typically 70-80% of native ARM performance for translated x86 code).

VM Networking

NAT Networking

Default mode; VM shares host’s network connection:

┌─────────────────────────────────────────┐
│              macOS Host                  │
│  ┌───────────────────────────────────┐  │
│  │     VM (192.168.64.x)             │  │
│  └─────────────┬─────────────────────┘  │
│                │ NAT                     │
│  ┌─────────────┴─────────────────────┐  │
│  │  Host Network (192.168.1.x)       │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘
# VM can access internet
# Host can't directly reach VM (port forwarding needed)

# Forward port 22 from host to VM (UTM)
# Configure in UTM VM settings → Network → Port Forward

Bridged Networking

VM appears as separate device on network:

┌────────────────────────────────────────────────┐
│                Network Switch                   │
├──────────────┬──────────────┬─────────────────┤
│ macOS Host   │     VM       │  Other Devices   │
│ 192.168.1.10 │ 192.168.1.20 │  192.168.1.x    │
└──────────────┴──────────────┴─────────────────┘
# VM gets own IP from DHCP
# Other network devices can reach VM directly
# Useful for testing network services

Host-Only Networking

Private network between host and VMs:

# VM can communicate with host and other VMs
# VM cannot access external network
# Good for isolated development environments

Shared Folders and File Sharing

VirtioFS (Fastest)

Available with Virtualization.framework:

# In Linux guest
$ sudo mount -t virtiofs sharename /mnt/shared

# Add to /etc/fstab
# sharename /mnt/shared virtiofs rw 0 0

SSHFS (Flexible)

Mount directories over SSH:

# On host, enable Remote Login (SSH)
# System Preferences → Sharing → Remote Login

# In Linux guest
$ sudo apt install sshfs
$ sshfs user@192.168.64.1:/Users/user/Projects /mnt/projects

# user@host-ip:/path /mount/point fuse.sshfs defaults 0 0

NFS (Traditional)

Reliable for larger setups:

# On macOS host, export directory
$ sudo bash -c 'echo "/Users/user/Shared -network 192.168.64.0 -mask 255.255.255.0" >> /etc/exports'
$ sudo nfsd restart

# In Linux guest
$ sudo mount -t nfs 192.168.64.1:/Users/user/Shared /mnt/shared

Development Workflow with VMs

Set Up SSH Access

# In VM, install and enable SSH
$ sudo apt install openssh-server
$ sudo systemctl enable ssh

# Get VM IP
$ hostname -I
192.168.64.5

# From macOS, add to ~/.ssh/config
Host dev-vm
    HostName 192.168.64.5
    User developer
    ForwardAgent yes

# Now connect easily
$ ssh dev-vm

VS Code Remote Development

# Install Remote - SSH extension in VS Code
# Connect to VM: Cmd+Shift+P → Remote-SSH: Connect to Host

# Or use devcontainers
# Define .devcontainer/devcontainer.json
# Use VM as Docker host

Synced Development

# Option 1: Edit on Mac, run in VM via shared folder
# Option 2: Full dev environment in VM, SSH from Mac
# Option 3: Use VS Code Remote for best of both

# Mount project from Mac to VM
$ sshfs user@192.168.64.1:/Users/user/Projects ~/projects
$ cd ~/projects/myapp
$ npm run dev

Performance Tips

Allocate Resources Appropriately

# For development VMs:
# - CPU: Half of physical cores (4 on 8-core Mac)
# - Memory: 8GB for comfortable IDE use
# - Disk: SSD, 50GB+ for development tools

# For testing/CI VMs:
# - CPU: 2 cores sufficient
# - Memory: 4GB usually enough
# - Disk: 20GB for minimal installs

Use Snapshots Wisely

# Create baseline snapshot after initial setup
# Use for quick rollback during testing
# Delete old snapshots to recover disk space

# Parallels
$ prlctl snapshot "VM" --name "Baseline"

# VMware
$ vmrun snapshot "/path/to/vm.vmx" "Baseline"

Headless Operation

# Run VMs headlessly for server workloads
# Less resource usage without GUI

# UTM: Window → Minimize or run via utmctl
# Parallels: prlctl start "VM" --headless
# VMware: vmrun start "/path/to/vm.vmx" nogui

Summary

SolutionLicenseBest ForApple Silicon
UTMFreePersonal use, testingGood (QEMU)
ParallelsCommercialDevelopers, Windows usersExcellent
VMware FusionCommercial/Free*Enterprise, existing VMwareGood
Virtualization.frameworkN/A (API)Custom solutions, Lima/TartNative

*Free personal use license available

Key considerations:

  1. Apple Silicon: Native ARM VMs are fast; x86 requires emulation or Rosetta
  2. File sharing: VirtioFS is fastest, SSHFS is most flexible
  3. Networking: NAT for isolation, bridged for network testing
  4. Snapshots: Essential for development and testing workflows
  5. Resources: Balance VM allocation with host system needs

Cross-Platform Script Compatibility

Writing shell scripts that work on both macOS and Linux is challenging. The differences between BSD and GNU utilities, path conventions, and available commands can break scripts that work perfectly on one platform. This chapter provides practical patterns for writing portable shell scripts that work across Unix-like systems.

The Compatibility Challenge

macOS and Linux differ in several ways that affect shell scripts:

┌───────────────────┬──────────────────────┬────────────────────────┐
│     Aspect        │       macOS          │        Linux           │
├───────────────────┼──────────────────────┼────────────────────────┤
│ Core utilities    │ BSD (FreeBSD-based)  │ GNU coreutils          │
│ Default shell     │ zsh (since Catalina) │ bash (usually)         │
│ sed in-place      │ sed -i ''            │ sed -i                 │
│ date command      │ BSD date             │ GNU date               │
│ readlink          │ Limited              │ Full (readlink -f)     │
│ stat format       │ stat -f "%..."       │ stat -c "%..."         │
│ grep              │ BSD grep             │ GNU grep               │
│ xargs             │ BSD xargs            │ GNU xargs              │
│ find              │ BSD find             │ GNU find               │
│ Bash version      │ 3.2 (old)            │ 4.x/5.x (current)      │
└───────────────────┴──────────────────────┴────────────────────────┘

Shebang Lines: The First Line Matters

The shebang line tells the system which interpreter to use.

Portable Shebang Patterns

#!/usr/bin/env bash    # Find bash in PATH - portable
#!/bin/bash            # Direct path - may not exist
#!/usr/bin/env sh      # POSIX shell - most portable
#!/usr/bin/env python3 # Python via PATH
#!/usr/bin/env perl    # Perl via PATH

Why Use /usr/bin/env

# Problem: bash location differs
# macOS: /bin/bash (ancient 3.2), /opt/homebrew/bin/bash (modern)
# Linux: /bin/bash or /usr/bin/bash

# Solution: let env find it
#!/usr/bin/env bash

# env searches PATH and runs the first match
# Users can control which version by modifying PATH

Caveats with env

# Can't pass arguments to interpreter with env (portably)
#!/usr/bin/env bash -e   # This won't work on all systems

# Instead, set options inside the script
#!/usr/bin/env bash
set -e  # Exit on error
set -u  # Error on undefined variables
set -o pipefail  # Pipeline fails if any command fails

Strict Mode Template

#!/usr/bin/env bash
#
# script-name.sh - Brief description
#

set -euo pipefail

# Debug mode (uncomment to enable)
# set -x

# Script directory (portable)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

Detecting the Operating System

Many scripts need to behave differently on different platforms.

Basic OS Detection

#!/usr/bin/env bash

detect_os() {
    case "$(uname -s)" in
        Darwin)
            OS="macos"
            ;;
        Linux)
            OS="linux"
            ;;
        FreeBSD)
            OS="freebsd"
            ;;
        CYGWIN*|MINGW*|MSYS*)
            OS="windows"
            ;;
        *)
            OS="unknown"
            ;;
    esac
    echo "$OS"
}

OS=$(detect_os)
echo "Running on: $OS"

Detailed Detection

#!/usr/bin/env bash

detect_platform() {
    local os arch distro

    # Operating system
    os=$(uname -s | tr '[:upper:]' '[:lower:]')

    # Architecture
    arch=$(uname -m)
    case "$arch" in
        x86_64|amd64)
            arch="amd64"
            ;;
        arm64|aarch64)
            arch="arm64"
            ;;
        armv7l)
            arch="arm"
            ;;
    esac

    # Linux distribution
    if [[ "$os" == "linux" ]]; then
        if [[ -f /etc/os-release ]]; then
            distro=$(. /etc/os-release && echo "$ID")
        elif [[ -f /etc/debian_version ]]; then
            distro="debian"
        elif [[ -f /etc/redhat-release ]]; then
            distro="rhel"
        else
            distro="unknown"
        fi
    fi

    echo "$os $arch ${distro:-}"
}

read -r OS ARCH DISTRO <<< "$(detect_platform)"
echo "OS: $OS, Arch: $ARCH, Distro: $DISTRO"

Using OS Detection

#!/usr/bin/env bash

OS=$(uname -s)

case "$OS" in
    Darwin)
        # macOS-specific commands
        OPEN_CMD="open"
        CLIPBOARD_COPY="pbcopy"
        CLIPBOARD_PASTE="pbpaste"
        ;;
    Linux)
        # Linux-specific commands
        OPEN_CMD="xdg-open"
        CLIPBOARD_COPY="xclip -selection clipboard"
        CLIPBOARD_PASTE="xclip -selection clipboard -o"
        ;;
esac

# Use the variables
echo "Hello" | $CLIPBOARD_COPY
$OPEN_CMD https://example.com

BSD vs GNU Command Differences

sed: In-Place Editing

The most common portability issue:

# macOS (BSD sed) requires suffix for -i
sed -i '' 's/old/new/g' file.txt

# Linux (GNU sed) requires no suffix (or suffix without space)
sed -i 's/old/new/g' file.txt

# Portable solution 1: Use backup extension
sed -i.bak 's/old/new/g' file.txt && rm file.txt.bak

# Portable solution 2: Detect and branch
if [[ "$(uname)" == "Darwin" ]]; then
    sed -i '' 's/old/new/g' file.txt
else
    sed -i 's/old/new/g' file.txt
fi

# Portable solution 3: Create a wrapper function
sedi() {
    if [[ "$(uname)" == "Darwin" ]]; then
        sed -i '' "$@"
    else
        sed -i "$@"
    fi
}

sedi 's/old/new/g' file.txt

date: Format Differences

# Get epoch timestamp
# macOS:
date +%s

# Get date from timestamp
# macOS (BSD date):
date -r 1609459200 "+%Y-%m-%d"

# Linux (GNU date):
date -d @1609459200 "+%Y-%m-%d"

# Portable approach:
epoch_to_date() {
    local epoch=$1
    local format=${2:-"%Y-%m-%d %H:%M:%S"}

    if date -r 0 &>/dev/null 2>&1; then
        # BSD date (macOS)
        date -r "$epoch" "+$format"
    else
        # GNU date (Linux)
        date -d "@$epoch" "+$format"
    fi
}

epoch_to_date 1609459200

# Date arithmetic
# macOS:
date -v+7d "+%Y-%m-%d"  # 7 days from now

# Linux:
date -d "+7 days" "+%Y-%m-%d"

# Portable date arithmetic:
days_from_now() {
    local days=$1
    local format=${2:-"%Y-%m-%d"}

    if date -v+1d &>/dev/null 2>&1; then
        date -v+"${days}d" "+$format"
    else
        date -d "+${days} days" "+$format"
    fi
}
# Get canonical path (resolve symlinks)
# Linux (GNU readlink):
readlink -f /some/path

# macOS: readlink doesn't have -f

# Portable solution 1: Use a function
realpath_portable() {
    if command -v realpath &>/dev/null; then
        realpath "$1"
    elif command -v greadlink &>/dev/null; then
        # GNU coreutils installed via Homebrew
        greadlink -f "$1"
    else
        # Pure bash fallback
        local path="$1"
        cd "$(dirname "$path")" 2>/dev/null || return 1
        path=$(pwd -P)/$(basename "$path")
        # Handle file symlinks
        while [[ -L "$path" ]]; do
            path=$(readlink "$path")
            cd "$(dirname "$path")" 2>/dev/null || return 1
            path=$(pwd -P)/$(basename "$path")
        done
        echo "$path"
    fi
}

# Portable solution 2: Python fallback
realpath() {
    python3 -c "import os; print(os.path.realpath('$1'))"
}

stat: File Information

# Get file size
# macOS (BSD stat):
stat -f %z file.txt

# Linux (GNU stat):
stat -c %s file.txt

# Portable:
file_size() {
    if stat -f %z "$1" &>/dev/null 2>&1; then
        stat -f %z "$1"
    else
        stat -c %s "$1"
    fi
}

# Get modification time (epoch)
# macOS:
stat -f %m file.txt

# Linux:
stat -c %Y file.txt

# Portable:
file_mtime() {
    if stat -f %m "$1" &>/dev/null 2>&1; then
        stat -f %m "$1"
    else
        stat -c %Y "$1"
    fi
}

grep: Extended Regex and Options

# Extended regex
# Both platforms support -E (POSIX):
grep -E 'pattern1|pattern2' file.txt

# Perl regex (different options)
# macOS: grep doesn't have -P
# Linux: grep -P uses PCRE

# Portable: use -E (ERE) instead of -P (PCRE)
# Or use Perl directly for complex patterns:
perl -ne 'print if /complex(?=pattern)/' file.txt

# Recursive grep
# Both support -r:
grep -r "pattern" directory/

# Count matches
# Both support -c:
grep -c "pattern" file.txt

# Fixed strings (no regex)
# Both support -F:
grep -F "literal.string" file.txt

xargs: Null Delimiter

# Handle filenames with spaces/newlines
# macOS and Linux both support -0:
find . -name "*.txt" -print0 | xargs -0 rm

# But -P (parallel) differs:
# Linux: xargs -P 4 (4 parallel processes)
# macOS: Same syntax, but may differ in behavior

# Portable parallel processing:
find . -name "*.txt" -print0 | xargs -0 -P "${JOBS:-4}" command

find: Differences

# Basic find works the same
find . -name "*.txt"

# Execute command
# Both support -exec:
find . -name "*.txt" -exec grep "pattern" {} \;

# Batch execute (less portable)
# GNU find: -exec command {} +
# BSD find: same, but edge cases differ

# Portable batch:
find . -name "*.txt" -print0 | xargs -0 grep "pattern"

# Delete files
# Both support -delete:
find . -name "*.tmp" -delete

# Time-based (syntax varies)
# Modified in last 7 days (both):
find . -mtime -7

# Minutes (both support -mmin):
find . -mmin -60

POSIX Compliance for Maximum Portability

POSIX Shell Basics

For maximum portability, write POSIX sh instead of bash:

#!/bin/sh
# POSIX-compliant shell script

# No arrays (bash feature)
# No [[ ]] (use [ ])
# No $(( )) arithmetic (use expr or bc)
# No process substitution <()
# No here-strings <<<

# POSIX test syntax
if [ "$var" = "value" ]; then
    echo "Match"
fi

# POSIX arithmetic
count=$(expr $count + 1)

# POSIX command substitution
result=$(command)

# POSIX string comparison
[ "$a" = "$b" ]   # Equal
[ "$a" != "$b" ]  # Not equal
[ -z "$a" ]       # Empty
[ -n "$a" ]       # Not empty

Bash vs POSIX Comparison

# Feature           | Bash              | POSIX sh
# ------------------|-------------------|------------------
# Arrays            | arr=(a b c)       | Not available
# Extended test     | [[ $a == b* ]]    | case statement
# Arithmetic        | $(( x + 1 ))      | expr / bc
# Here-string       | cmd <<< "$var"    | echo "$var" | cmd
# Process subst     | diff <(cmd1) <(cmd2) | temp files
# Regex matching    | [[ $x =~ regex ]] | grep / expr

# POSIX alternatives:

# Instead of [[ $str == pattern* ]]
case "$str" in
    pattern*) echo "Match" ;;
    *) echo "No match" ;;
esac

# Instead of arrays
set -- value1 value2 value3
for item in "$@"; do
    echo "$item"
done

# Instead of here-string
echo "$var" | command
# or
printf '%s\n' "$var" | command

# Instead of process substitution
cmd1 > /tmp/file1.$$
cmd2 > /tmp/file2.$$
diff /tmp/file1.$$ /tmp/file2.$$
rm /tmp/file1.$$ /tmp/file2.$$

Portable Script Patterns

Temporary Files

#!/usr/bin/env bash

# Create temp file portably
if command -v mktemp &>/dev/null; then
    TMPFILE=$(mktemp)
else
    TMPFILE="/tmp/script.$$.$RANDOM"
    touch "$TMPFILE"
fi

# Cleanup on exit
cleanup() {
    rm -f "$TMPFILE"
}
trap cleanup EXIT

# Use temp file
echo "data" > "$TMPFILE"

Command Availability

#!/usr/bin/env bash

# Check if command exists
command_exists() {
    command -v "$1" &>/dev/null
}

# Require commands
require_commands() {
    local missing=()
    for cmd in "$@"; do
        if ! command_exists "$cmd"; then
            missing+=("$cmd")
        fi
    done

    if [[ ${#missing[@]} -gt 0 ]]; then
        echo "Error: Missing required commands: ${missing[*]}" >&2
        exit 1
    fi
}

require_commands git curl jq

# Find alternative commands
find_command() {
    for cmd in "$@"; do
        if command_exists "$cmd"; then
            echo "$cmd"
            return 0
        fi
    done
    return 1
}

# Find a download tool
DOWNLOAD_CMD=$(find_command curl wget fetch)
if [[ -z "$DOWNLOAD_CMD" ]]; then
    echo "No download tool found"
    exit 1
fi

Cross-Platform Download

#!/usr/bin/env bash

download() {
    local url=$1
    local dest=$2

    if command -v curl &>/dev/null; then
        curl -fsSL -o "$dest" "$url"
    elif command -v wget &>/dev/null; then
        wget -q -O "$dest" "$url"
    elif command -v fetch &>/dev/null; then
        fetch -q -o "$dest" "$url"
    else
        echo "No download tool found" >&2
        return 1
    fi
}

download "https://example.com/file.tar.gz" "file.tar.gz"

Getting Script Directory

#!/usr/bin/env bash

# Works in bash (handles symlinks, sourced scripts)
get_script_dir() {
    local source="${BASH_SOURCE[0]}"
    local dir

    # Resolve symlinks
    while [[ -L "$source" ]]; do
        dir=$(cd -P "$(dirname "$source")" && pwd)
        source=$(readlink "$source")
        # Handle relative symlinks
        [[ $source != /* ]] && source="$dir/$source"
    done

    cd -P "$(dirname "$source")" && pwd
}

SCRIPT_DIR=$(get_script_dir)
echo "Script is in: $SCRIPT_DIR"

# POSIX version (simpler, no symlink resolution)
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)

User Input with Defaults

#!/usr/bin/env bash

# Read with default (works in bash and zsh)
prompt() {
    local message=$1
    local default=$2
    local response

    if [[ -n "$default" ]]; then
        read -r -p "$message [$default]: " response
        echo "${response:-$default}"
    else
        read -r -p "$message: " response
        echo "$response"
    fi
}

# Handle non-interactive environments
prompt_or_default() {
    local varname=$1
    local message=$2
    local default=$3

    if [[ -t 0 ]]; then
        # Interactive
        read -r -p "$message [$default]: " value
        eval "$varname='${value:-$default}'"
    else
        # Non-interactive
        eval "$varname='$default'"
    fi
}

NAME=$(prompt "Enter your name" "Anonymous")

Color Output

#!/usr/bin/env bash

# Check if terminal supports colors
supports_colors() {
    [[ -t 1 ]] && [[ -n "$TERM" ]] && [[ "$TERM" != "dumb" ]]
}

# Set up colors (or empty strings if not supported)
setup_colors() {
    if supports_colors; then
        RED='\033[0;31m'
        GREEN='\033[0;32m'
        YELLOW='\033[0;33m'
        BLUE='\033[0;34m'
        BOLD='\033[1m'
        NC='\033[0m'  # No Color
    else
        RED=''
        GREEN=''
        YELLOW=''
        BLUE=''
        BOLD=''
        NC=''
    fi
}

setup_colors

# Use colors
echo -e "${RED}Error:${NC} Something went wrong"
echo -e "${GREEN}Success:${NC} Operation completed"
echo -e "${YELLOW}Warning:${NC} Check this"
echo -e "${BLUE}Info:${NC} FYI"

# Or use printf (more portable)
log_error() { printf "${RED}Error:${NC} %s\n" "$*" >&2; }
log_success() { printf "${GREEN}Success:${NC} %s\n" "$*"; }
log_warn() { printf "${YELLOW}Warning:${NC} %s\n" "$*"; }
log_info() { printf "${BLUE}Info:${NC} %s\n" "$*"; }

Complete Portable Script Template

#!/usr/bin/env bash
#
# portable-script.sh - A cross-platform script template
#
# Usage: portable-script.sh [options] <arguments>
#

set -euo pipefail

# Script metadata
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_VERSION="1.0.0"

# Get script directory (handles symlinks)
get_script_dir() {
    local source="${BASH_SOURCE[0]}"
    while [[ -L "$source" ]]; do
        local dir=$(cd -P "$(dirname "$source")" && pwd)
        source=$(readlink "$source")
        [[ $source != /* ]] && source="$dir/$source"
    done
    cd -P "$(dirname "$source")" && pwd
}
readonly SCRIPT_DIR=$(get_script_dir)

# Detect OS
detect_os() {
    case "$(uname -s)" in
        Darwin) echo "macos" ;;
        Linux) echo "linux" ;;
        *) echo "unknown" ;;
    esac
}
readonly OS=$(detect_os)

# Colors
setup_colors() {
    if [[ -t 1 ]] && [[ -n "${TERM:-}" ]] && [[ "${TERM}" != "dumb" ]]; then
        RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'
        BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
    else
        RED=''; GREEN=''; YELLOW=''; BLUE=''; BOLD=''; NC=''
    fi
}
setup_colors

# Logging functions
log_info() { printf "${BLUE}[INFO]${NC} %s\n" "$*"; }
log_success() { printf "${GREEN}[OK]${NC} %s\n" "$*"; }
log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*" >&2; }
log_error() { printf "${RED}[ERROR]${NC} %s\n" "$*" >&2; }
die() { log_error "$*"; exit 1; }

# Command check
require_cmd() {
    command -v "$1" &>/dev/null || die "Required command not found: $1"
}

# Cross-platform sed -i
sedi() {
    if [[ "$OS" == "macos" ]]; then
        sed -i '' "$@"
    else
        sed -i "$@"
    fi
}

# Cleanup
cleanup() {
    # Remove temp files, etc.
    :
}
trap cleanup EXIT

# Usage
usage() {
    cat << EOF
Usage: $SCRIPT_NAME [options] <argument>

A portable cross-platform script template.

Options:
    -h, --help      Show this help message
    -v, --version   Show version
    -d, --debug     Enable debug mode

Examples:
    $SCRIPT_NAME file.txt
    $SCRIPT_NAME --debug file.txt
EOF
}

# Parse arguments
DEBUG=false
POSITIONAL_ARGS=()

while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            usage
            exit 0
            ;;
        -v|--version)
            echo "$SCRIPT_NAME version $SCRIPT_VERSION"
            exit 0
            ;;
        -d|--debug)
            DEBUG=true
            set -x
            shift
            ;;
        -*)
            die "Unknown option: $1"
            ;;
        *)
            POSITIONAL_ARGS+=("$1")
            shift
            ;;
    esac
done

set -- "${POSITIONAL_ARGS[@]}"

# Main logic
main() {
    log_info "Running on $OS"
    log_info "Script directory: $SCRIPT_DIR"

    if [[ $# -lt 1 ]]; then
        die "Missing required argument. Use --help for usage."
    fi

    local input=$1
    log_info "Processing: $input"

    # Your code here

    log_success "Done!"
}

main "$@"

Testing Script Portability

Using ShellCheck

# Install ShellCheck
$ brew install shellcheck   # macOS
$ apt install shellcheck    # Debian/Ubuntu

# Check script for issues
$ shellcheck script.sh

# Check for POSIX compliance
$ shellcheck --shell=sh script.sh

# Exclude specific warnings
$ shellcheck --exclude=SC2086 script.sh

Testing on Multiple Platforms

# Test in Docker containers
$ docker run --rm -v "$PWD:/scripts" ubuntu:22.04 bash /scripts/test.sh
$ docker run --rm -v "$PWD:/scripts" alpine:3 sh /scripts/test.sh

# Test with different shells
$ bash script.sh
$ zsh script.sh
$ dash script.sh   # POSIX test

Summary

ChallengePortable Solution
Shebang#!/usr/bin/env bash
OS detectionuname -s + case statement
sed in-placesed -i.bak + rm, or wrapper function
date formattingOS-specific wrapper function
readlink -fPure bash function or Python fallback
stat formatOS-specific wrapper function
Script directoryBASH_SOURCE[0] with symlink resolution
ColorsCheck [[ -t 1 ]] and $TERM
DownloadsCheck for curl/wget/fetch
Temp filesmktemp with fallback

Key practices:

  1. Use #!/usr/bin/env bash for bash scripts
  2. Detect OS early and branch for platform-specific code
  3. Create wrapper functions for incompatible commands
  4. Test with ShellCheck and on multiple platforms
  5. Use POSIX sh for maximum portability when bash features aren’t needed
  6. Document platform requirements in script header

SSH Configuration and Usage

SSH (Secure Shell) is fundamental to modern development workflows. Whether you’re deploying to servers, managing cloud infrastructure, or pushing code to GitHub, SSH handles the secure communication. macOS includes a full OpenSSH implementation, and understanding its configuration can dramatically improve your productivity.

SSH Basics

How SSH Works

┌─────────────────────────────────────────────────────────────────┐
│                    SSH Connection                                │
│                                                                  │
│  ┌─────────────┐           Encrypted Channel          ┌─────────────┐
│  │  SSH Client │ ◄─────────────────────────────────► │  SSH Server │
│  │   (macOS)   │                                      │   (Remote)  │
│  └─────────────┘                                      └─────────────┘
│                                                                  │
│  Authentication Methods:                                         │
│  1. Public Key (recommended)                                     │
│  2. Password (fallback)                                          │
│  3. Keyboard-interactive                                         │
│  4. Certificate-based                                            │
└─────────────────────────────────────────────────────────────────┘

Basic SSH Connection

# Connect to a remote host
$ ssh user@hostname

# Connect on non-standard port
$ ssh -p 2222 user@hostname

# Connect with specific identity (key)
$ ssh -i ~/.ssh/specific_key user@hostname

# Run a command remotely
$ ssh user@hostname "ls -la /var/log"

# Run command and stay connected
$ ssh -t user@hostname "cd /app && bash"

SSH Key Management

Generating SSH Keys

# Generate Ed25519 key (recommended for modern systems)
$ ssh-keygen -t ed25519 -C "your_email@example.com"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/Users/user/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:

# Generate RSA key (broader compatibility)
$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

# Generate key with specific filename
$ ssh-keygen -t ed25519 -f ~/.ssh/github_key -C "github key"

# View public key
$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG... your_email@example.com

Key Types Comparison

TypeSecurityCompatibilityRecommendation
Ed25519ExcellentModern systemsRecommended
RSA 4096Very goodUniversalLegacy/compatibility
ECDSAGoodWideAlternative
DSADeprecatedOld systemsAvoid

Installing Keys on Remote Servers

# Using ssh-copy-id (easiest)
$ ssh-copy-id user@hostname
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/Users/user/.ssh/id_ed25519.pub"
user@hostname's password:
Number of key(s) added: 1

# Specify a particular key
$ ssh-copy-id -i ~/.ssh/specific_key.pub user@hostname

# Manual method (if ssh-copy-id unavailable)
$ cat ~/.ssh/id_ed25519.pub | ssh user@hostname "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

# Copy key to GitHub via API
$ cat ~/.ssh/id_ed25519.pub | pbcopy
# Then paste in GitHub Settings → SSH keys

Key Permissions

SSH is strict about file permissions:

# Set correct permissions
$ chmod 700 ~/.ssh
$ chmod 600 ~/.ssh/id_ed25519
$ chmod 644 ~/.ssh/id_ed25519.pub
$ chmod 600 ~/.ssh/config
$ chmod 600 ~/.ssh/authorized_keys

# Fix permissions recursively
$ chmod 700 ~/.ssh && chmod 600 ~/.ssh/*

# Verify permissions
$ ls -la ~/.ssh
drwx------   8 user  staff   256 Jan  1 12:00 .
-rw-------   1 user  staff   464 Jan  1 12:00 id_ed25519
-rw-r--r--   1 user  staff   100 Jan  1 12:00 id_ed25519.pub
-rw-------   1 user  staff   500 Jan  1 12:00 config

SSH Configuration File

The SSH config file (~/.ssh/config) is the key to efficient SSH usage.

Basic Configuration

# ~/.ssh/config

# Default settings for all hosts
Host *
    AddKeysToAgent yes
    UseKeychain yes  # macOS-specific: store passphrase in Keychain
    IdentitiesOnly yes
    ServerAliveInterval 60
    ServerAliveCountMax 3

# Work server
Host work
    HostName work-server.example.com
    User admin
    Port 22
    IdentityFile ~/.ssh/work_key

# Personal server
Host personal
    HostName my-server.example.com
    User myuser
    IdentityFile ~/.ssh/personal_key
    ForwardAgent yes

# Connect with: ssh work
#          or: ssh personal

Pattern Matching

# Match multiple hosts with patterns
Host dev-*
    User developer
    IdentityFile ~/.ssh/dev_key

Host prod-*
    User deployer
    IdentityFile ~/.ssh/prod_key
    ForwardAgent no

# Specific server overrides pattern
Host dev-db
    HostName dev-database.example.com
    LocalForward 5432 localhost:5432

# Matches: ssh dev-web, ssh dev-api, ssh prod-web, etc.

SSH Config Options Reference

OptionDescriptionExample
HostNameReal hostname/IPHostName 192.168.1.100
UserLogin usernameUser admin
PortSSH portPort 2222
IdentityFilePath to private keyIdentityFile ~/.ssh/key
IdentitiesOnlyOnly use specified keysIdentitiesOnly yes
ForwardAgentForward ssh-agentForwardAgent yes
ProxyJumpJump through hostProxyJump bastion
LocalForwardForward local portLocalForward 8080 localhost:80
RemoteForwardForward remote portRemoteForward 9090 localhost:9090
DynamicForwardSOCKS proxyDynamicForward 1080
ServerAliveIntervalKeep-alive intervalServerAliveInterval 60
CompressionEnable compressionCompression yes
StrictHostKeyCheckingHost key checkingStrictHostKeyChecking ask

SSH Agent

The SSH agent holds your decrypted private keys in memory, so you don’t have to enter your passphrase repeatedly.

macOS SSH Agent

macOS has an integrated ssh-agent that works with Keychain:

# Start ssh-agent (usually automatic on macOS)
$ eval "$(ssh-agent -s)"
Agent pid 12345

# Add key to agent
$ ssh-add ~/.ssh/id_ed25519
Enter passphrase for /Users/user/.ssh/id_ed25519:
Identity added: /Users/user/.ssh/id_ed25519

# Add key and store passphrase in Keychain (macOS-specific)
$ ssh-add --apple-use-keychain ~/.ssh/id_ed25519

# List keys in agent
$ ssh-add -l
256 SHA256:abc123... user@email.com (ED25519)

# Remove all keys from agent
$ ssh-add -D
All identities removed.

# Remove specific key
$ ssh-add -d ~/.ssh/id_ed25519

Automatic Key Loading

Configure in ~/.ssh/config:

Host *
    AddKeysToAgent yes
    UseKeychain yes  # macOS: store passphrase in Keychain
    IdentityFile ~/.ssh/id_ed25519

With this configuration:

  1. First connection prompts for passphrase
  2. Passphrase is stored in macOS Keychain
  3. Subsequent connections use stored passphrase

Agent Forwarding

Forward your local SSH agent to remote servers:

# Enable in config
Host server
    ForwardAgent yes

# Or on command line
$ ssh -A user@server

# On the remote server, you can now SSH to other servers
# using your local keys
user@server$ ssh git@github.com
# Works without having keys on the server

Security Warning: Only enable agent forwarding to trusted servers. A compromised server could use your forwarded agent.

Managing Multiple Keys

# ~/.ssh/config - different keys for different services

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_key
    IdentitiesOnly yes

Host gitlab.com
    HostName gitlab.com
    User git
    IdentityFile ~/.ssh/gitlab_key
    IdentitiesOnly yes

Host bitbucket.org
    HostName bitbucket.org
    User git
    IdentityFile ~/.ssh/bitbucket_key
    IdentitiesOnly yes

# Work servers use work key
Host *.work.example.com
    IdentityFile ~/.ssh/work_key

# Personal servers use personal key
Host *.personal.example.com
    IdentityFile ~/.ssh/personal_key

ProxyJump: Jumping Through Bastion Hosts

Many networks require connecting through a bastion (jump) host:

┌─────────────┐        ┌─────────────┐        ┌─────────────┐
│  Your Mac   │──SSH──▶│   Bastion   │──SSH──▶│ Target Host │
│             │        │  (Public)   │        │  (Private)  │
└─────────────┘        └─────────────┘        └─────────────┘

Using ProxyJump

# Command line
$ ssh -J bastion.example.com user@internal-server

# Multiple jumps
$ ssh -J jump1.example.com,jump2.example.com user@target

# In config file
Host bastion
    HostName bastion.example.com
    User admin

Host internal-*
    ProxyJump bastion
    User developer

Host internal-web
    HostName 10.0.1.10

Host internal-db
    HostName 10.0.1.20
    LocalForward 5432 localhost:5432

# Now connect directly:
$ ssh internal-web
# Automatically jumps through bastion

Legacy ProxyCommand

For older SSH versions or complex scenarios:

Host internal-*
    ProxyCommand ssh -W %h:%p bastion.example.com

# Using netcat through bastion
Host legacy-internal
    ProxyCommand ssh bastion.example.com nc %h %p

Connection Multiplexing

Multiplexing shares a single TCP connection for multiple SSH sessions, dramatically speeding up subsequent connections.

Enabling Multiplexing

# ~/.ssh/config
Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600  # Keep connection for 10 minutes

# Create socket directory
$ mkdir -p ~/.ssh/sockets
$ chmod 700 ~/.ssh/sockets

How It Works

# First connection: establishes TCP connection + SSH handshake
$ ssh server
# Takes ~500ms for key exchange, etc.

# Second connection: reuses existing connection
$ ssh server
# Takes ~50ms - almost instant

# View active control sockets
$ ls ~/.ssh/sockets
user@server-22

# Check control socket status
$ ssh -O check server
Master running (pid=12345)

# Close control socket
$ ssh -O exit server
Exit request sent.

Multiplexing Options

OptionDescription
ControlMaster autoCreate master if none exists
ControlMaster yesAlways create master
ControlMaster noDon’t use multiplexing
ControlPersist 600Keep master for 600 seconds
ControlPersist yesKeep master indefinitely

Port Forwarding (Tunneling)

Local Port Forwarding

Forward a local port to access a remote service:

# Access remote MySQL through local port
$ ssh -L 3306:localhost:3306 user@server
# Now connect to localhost:3306 to reach server's MySQL

# Access internal service through bastion
$ ssh -L 8080:internal-app:80 bastion
# localhost:8080 → bastion → internal-app:80

# In config file
Host db-tunnel
    HostName db-server.example.com
    User admin
    LocalForward 5432 localhost:5432
    LocalForward 6379 localhost:6379

# Multiple forwards
$ ssh -L 5432:localhost:5432 -L 6379:localhost:6379 server

Remote Port Forwarding

Expose a local service to the remote server:

# Make local web server accessible on remote
$ ssh -R 8080:localhost:3000 server
# server:8080 → your machine:3000

# Allow external connections to forwarded port
$ ssh -R 0.0.0.0:8080:localhost:3000 server
# Requires GatewayPorts yes on server

# In config
Host expose-local
    HostName server.example.com
    RemoteForward 8080 localhost:3000

Dynamic Port Forwarding (SOCKS Proxy)

# Create SOCKS5 proxy
$ ssh -D 1080 server

# Configure applications to use localhost:1080 as SOCKS proxy
# All traffic through that proxy goes via the SSH connection

# In config
Host proxy
    HostName server.example.com
    DynamicForward 1080

# Use with curl
$ curl --socks5 localhost:1080 https://example.com

Practical SSH Configurations

Developer Setup

# ~/.ssh/config - Complete developer configuration

# Global settings
Host *
    AddKeysToAgent yes
    UseKeychain yes
    IdentitiesOnly yes
    ServerAliveInterval 60
    ServerAliveCountMax 3
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

# GitHub
Host github.com
    User git
    IdentityFile ~/.ssh/github_ed25519

# GitLab
Host gitlab.com
    User git
    IdentityFile ~/.ssh/gitlab_ed25519

# Work infrastructure
Host bastion
    HostName bastion.work.example.com
    User myuser
    IdentityFile ~/.ssh/work_key

Host work-*
    ProxyJump bastion
    User myuser
    IdentityFile ~/.ssh/work_key

Host work-web
    HostName 10.0.1.10

Host work-api
    HostName 10.0.1.20

Host work-db
    HostName 10.0.1.30
    LocalForward 5432 localhost:5432
    LocalForward 6379 localhost:6379

# Personal servers
Host vps
    HostName my-vps.example.com
    User root
    IdentityFile ~/.ssh/personal_key
    ForwardAgent yes

# Raspberry Pi
Host pi
    HostName raspberrypi.local
    User pi
    IdentityFile ~/.ssh/pi_key

CI/CD Access

# Service accounts with restricted access
Host deploy-*
    User deploy
    IdentityFile ~/.ssh/deploy_key
    IdentitiesOnly yes
    ForwardAgent no
    RequestTTY no

Host deploy-prod
    HostName prod.example.com
    # No port forwarding
    PermitLocalCommand no

SSH Security Best Practices

Key Security

# Use strong key types
$ ssh-keygen -t ed25519  # Recommended
$ ssh-keygen -t rsa -b 4096  # If Ed25519 not supported

# Always use passphrases on keys
# Store passphrases in macOS Keychain

# Rotate keys periodically
# Keep separate keys for different purposes

Configuration Security

# Disable password authentication (on servers you control)
# /etc/ssh/sshd_config
PasswordAuthentication no
PubkeyAuthentication yes

# On client, be cautious with host key checking
Host trusted-server
    StrictHostKeyChecking yes  # Fail if key changes

Host new-servers
    StrictHostKeyChecking ask  # Prompt user

# Never use:
# StrictHostKeyChecking no  # DANGEROUS

Audit SSH Usage

# View SSH authentication logs (macOS)
$ log show --predicate 'subsystem == "com.openssh.sshd"' --last 1h

# View all SSH attempts
$ log show --predicate 'processImagePath contains "ssh"' --last 1h

# Check authorized keys
$ cat ~/.ssh/authorized_keys

# List fingerprints of authorized keys
$ while read -r line; do echo "$line" | ssh-keygen -lf -; done < ~/.ssh/authorized_keys

Troubleshooting SSH

Verbose Output

# Increase verbosity
$ ssh -v user@host    # Verbose
$ ssh -vv user@host   # More verbose
$ ssh -vvv user@host  # Maximum verbosity

# Common issues revealed:
# - Key not being offered
# - Permission problems
# - Authentication method issues

Common Issues

# Permission denied (publickey)
# Check:
$ ssh-add -l  # Is key loaded in agent?
$ ls -la ~/.ssh/  # Are permissions correct?
$ ssh -vv user@host  # What's actually happening?

# Host key verification failed
# The server's key changed (or MITM attack)
$ ssh-keygen -R hostname  # Remove old key
$ ssh user@hostname  # Accept new key

# Connection refused
# Check if SSH is running on server
$ nc -zv hostname 22

# Timeout issues
# Add keep-alive settings
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

# Too many authentication failures
# You're trying too many keys
Host specific-server
    IdentitiesOnly yes
    IdentityFile ~/.ssh/specific_key

Debug SSH Config

# See effective configuration for a host
$ ssh -G hostname
user developer
hostname internal.example.com
port 22
identityfile /Users/user/.ssh/work_key
proxyjump bastion.example.com

# Test connection without connecting
$ ssh -T git@github.com
Hi username! You've successfully authenticated...

Summary

TaskCommand/Config
Generate keyssh-keygen -t ed25519
Add to agentssh-add ~/.ssh/key
Copy keyssh-copy-id user@host
Quick connectConfigure in ~/.ssh/config
Jump hostProxyJump bastion
Port forwardLocalForward 5432 localhost:5432
MultiplexingControlMaster auto
Debugssh -vvv user@host

Key practices:

  1. Use Ed25519 keys with strong passphrases
  2. Configure ~/.ssh/config for all regular hosts
  3. Use SSH agent with Keychain integration
  4. Enable multiplexing for faster connections
  5. Use ProxyJump for bastion access
  6. Keep separate keys for different purposes
  7. Regularly audit authorized_keys

Remote Access: VNC and ARD

Beyond SSH for command-line access, developers often need graphical remote access to machines. macOS includes built-in screen sharing capabilities, while accessing Linux systems typically uses VNC. This chapter covers the various remote access options available, focusing on practical setups for development workflows.

macOS Screen Sharing Technologies

macOS provides multiple technologies for remote desktop access:

┌─────────────────────────────────────────────────────────────────┐
│                 macOS Remote Access Options                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Screen Sharing (VNC)     Apple Remote Desktop     SSH + X11    │
│  ├── Built-in service     ├── Commercial product  ├── X11 apps  │
│  ├── VNC compatible       ├── Mass management     ├── Headless  │
│  └── Basic features       └── Enterprise tools    └── Tunneled  │
│                                                                  │
│  Third-party clients                                             │
│  ├── Remote Desktop (Microsoft)                                  │
│  ├── TeamViewer, AnyDesk                                         │
│  └── Chrome Remote Desktop                                       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Screen Sharing: macOS Built-in VNC

Enabling Screen Sharing

Through System Settings:

  1. System Settings → General → Sharing
  2. Enable “Screen Sharing”
  3. Configure allowed users

Via command line:

# Enable Screen Sharing
$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist

# Check status
$ sudo launchctl list | grep screensharing
-       0       com.apple.screensharing

# Disable Screen Sharing
$ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.screensharing.plist

# Alternative using systemsetup (older method)
$ sudo systemsetup -setremotelogin on  # SSH
# Note: Screen Sharing requires System Settings

Connecting from macOS to macOS

# Using Finder
# Go → Connect to Server (Cmd+K)
# vnc://hostname.local
# or vnc://192.168.1.100

# Using open command
$ open vnc://hostname.local
$ open vnc://user@hostname.local

# Using Screen Sharing app directly
$ open -a "Screen Sharing" --args vnc://hostname.local

# Via Bonjour (automatic discovery)
# Finder sidebar shows nearby Macs automatically

VNC Port and Firewall

Screen Sharing uses:

  • Port 5900: VNC display 0
  • Port 3283: Apple Remote Desktop agent
# Check if Screen Sharing is listening
$ lsof -i :5900
screenshar 1234 root    4u  IPv6 0x123456789      0t0  TCP *:rfb (LISTEN)

# Allow through firewall (if needed)
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /System/Library/CoreServices/Screen\ Sharing.app

Connecting from Terminal (headless)

# Using open command from SSH session
$ ssh user@mac
user@mac$ open vnc://localhost  # Won't work headless

# For automated connections, use ARD's kickstart
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -activate -configure -allowAccessFor -allUsers -privs -all

Apple Remote Desktop (ARD)

Apple Remote Desktop is a commercial tool for managing multiple Macs. Even without buying ARD, you can use some of its features.

Enabling Remote Management

# Enable Remote Management (more features than Screen Sharing)
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -activate

# Full configuration
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -activate \
    -configure -allowAccessFor -specifiedUsers \
    -access -on \
    -privs -all \
    -users admin

# Enable for all users
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -activate -configure -allowAccessFor -allUsers -privs -all

# Disable Remote Management
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -deactivate -stop

# Check status
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -getaccess

ARD Privileges

Different privilege levels can be assigned:

# -privs options:
# -all               All privileges
# -none              No privileges
# -TextMessages      Text messages
# -ControlObserve    Control and observe
# -SendFiles         Send files
# -DeleteFiles       Delete files
# -GenerateReports   Generate reports
# -OpenQuitApps      Open and quit apps
# -ChangeSettings    Change settings
# -RestartShutdown   Restart and shutdown
# -ShowObserve       Observe only

# Example: Control and observe only
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -configure -allowAccessFor -specifiedUsers \
    -privs -ControlObserve -users developer

Accessing Mac Remotely Over the Internet

Using SSH Tunneling

The most secure method for remote VNC access:

# On remote Mac, ensure SSH and Screen Sharing are enabled

# From your laptop, create SSH tunnel
$ ssh -L 5901:localhost:5900 user@mac-at-home.example.com

# Connect VNC to the tunnel
$ open vnc://localhost:5901

# Or combined in one step
$ ssh -f -N -L 5901:localhost:5900 user@mac.example.com && open vnc://localhost:5901

SSH Config for VNC Tunnel

# ~/.ssh/config
Host mac-remote
    HostName mac.example.com
    User myuser
    LocalForward 5901 localhost:5900

# Usage:
$ ssh -f -N mac-remote  # Create tunnel in background
$ open vnc://localhost:5901

Using iCloud Back to My Mac (Legacy)

Back to My Mac was discontinued, but similar functionality exists via iCloud:

# Modern approach: Use iCloud Private Relay + Screen Sharing
# Requires both Macs signed into same iCloud account
# Find in Finder sidebar or use address:
# vnc://[Apple ID email]@[Computer Name].local

# Note: This only works on local network now
# For internet access, use SSH tunnel or third-party solutions

Third-Party Solutions

For easier remote access over the internet:

# Install TeamViewer
$ brew install --cask teamviewer

# Install AnyDesk
$ brew install --cask anydesk

# Chrome Remote Desktop
# Install Chrome extension and enable remote access

# Tailscale (VPN mesh network)
$ brew install --cask tailscale
# Create overlay network, then use normal VNC
$ open vnc://mac-remote.tailnet-xxxx.ts.net

Connecting to Linux Systems

VNC to Linux

Linux systems typically use TigerVNC, RealVNC, x11vnc, or similar.

# Connect to Linux VNC server from macOS
$ open vnc://linux-server:5901

# Or use a VNC client
$ brew install --cask vnc-viewer  # RealVNC viewer
$ brew install --cask tigervnc-viewer

# Command-line VNC client
$ brew install tiger-vnc
$ vncviewer linux-server:1

Setting Up VNC on Linux

# On Linux (Ubuntu example)

# Install TigerVNC server
$ sudo apt install tigervnc-standalone-server

# Set VNC password
$ vncpasswd

# Start VNC server (display :1 = port 5901)
$ vncserver :1 -geometry 1920x1080 -depth 24

# Configure to start on boot (systemd)
$ sudo vim /etc/systemd/system/vncserver@.service

Example systemd service:

[Unit]
Description=VNC Server at display %i
After=syslog.target network.target

[Service]
Type=forking
User=developer
ExecStart=/usr/bin/vncserver :%i -geometry 1920x1080 -depth 24
ExecStop=/usr/bin/vncserver -kill :%i

[Install]
WantedBy=multi-user.target
# Enable and start
$ sudo systemctl enable vncserver@1
$ sudo systemctl start vncserver@1

Secure VNC with SSH Tunnel

VNC traffic is unencrypted by default. Always tunnel through SSH:

# ~/.ssh/config
Host linux-vnc
    HostName linux-server.example.com
    User developer
    LocalForward 5901 localhost:5901

# Connect
$ ssh -f -N linux-vnc
$ open vnc://localhost:5901

x11vnc: Share Existing Display

x11vnc shares the actual display (like Screen Sharing on Mac):

# On Linux
$ sudo apt install x11vnc

# Share current display
$ x11vnc -display :0 -forever -loop -shared -rfbauth ~/.vnc/passwd

# With more options
$ x11vnc -display :0 \
    -forever \
    -loop \
    -shared \
    -rfbauth ~/.vnc/passwd \
    -ncache 10 \
    -ncache_cr \
    -noxdamage

RDP: Connecting to Windows

Microsoft Remote Desktop

# Install Microsoft Remote Desktop
$ brew install --cask microsoft-remote-desktop

# Or from Mac App Store

RDP provides better performance than VNC for Windows connections.

Connecting to Windows from Terminal

# Using FreeRDP (open source RDP client)
$ brew install freerdp

# Connect to Windows
$ xfreerdp /u:username /p:password /v:windows-pc /size:1920x1080

# With additional options
$ xfreerdp /u:username /v:windows-pc \
    /size:1920x1080 \
    /bpp:32 \
    /audio-mode:0 \
    /clipboard

RDP from macOS to Linux

Some Linux systems support RDP via xrdp:

# On Linux
$ sudo apt install xrdp
$ sudo systemctl enable xrdp
$ sudo systemctl start xrdp

# From macOS, use Microsoft Remote Desktop
# Connect to linux-server.local:3389

X11 Forwarding

For running individual graphical Linux apps on your Mac:

Enabling X11 Forwarding

# Install XQuartz (X11 for macOS)
$ brew install --cask xquartz

# Log out and log back in (or restart)

# Enable X11 forwarding in SSH config
Host linux-x11
    HostName linux-server.example.com
    ForwardX11 yes
    ForwardX11Trusted yes

# Or on command line
$ ssh -X linux-server  # Basic X11 forwarding
$ ssh -Y linux-server  # Trusted X11 forwarding (less secure but more compatible)

Running X11 Apps

# Connect with X11 forwarding
$ ssh -Y user@linux-server

# Run graphical application
user@linux$ firefox &
user@linux$ gedit myfile.txt &

# The window appears on your Mac

X11 Compression

For slow connections, enable compression:

$ ssh -Y -C user@linux-server

# In config
Host linux-slow
    HostName linux-server.example.com
    ForwardX11 yes
    ForwardX11Trusted yes
    Compression yes

Remote Access Best Practices

Security Recommendations

# 1. Never expose VNC directly to the internet
# Always use SSH tunneling or VPN

# 2. Use strong VNC passwords
$ vncpasswd  # On Linux
# macOS uses your user password

# 3. Limit access to specific users
# System Settings → Sharing → Screen Sharing → Only these users

# 4. Enable firewall
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on

# 5. Use Tailscale or similar for secure remote access
$ brew install --cask tailscale
# Creates encrypted WireGuard mesh network

Performance Optimization

# For slow connections:

# 1. Reduce color depth
# In VNC client, use 256 colors or grayscale

# 2. Enable compression
$ ssh -Y -C user@linux-server

# 3. Reduce resolution
# Remote: System Settings → Displays → Scaled → Lower resolution

# 4. Disable animations
# Remote: System Settings → Accessibility → Display → Reduce motion

# 5. Use a dedicated VNC protocol (not VNC over HTTP)

Headless Mac Administration

For Macs without displays (servers, CI machines):

# Enable SSH
$ sudo systemsetup -setremotelogin on

# Enable Screen Sharing headless
$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist

# Enable Remote Management
$ sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
    -activate -configure -allowAccessFor -allUsers -privs -all

# Create a virtual display (if needed for some apps)
# Some apps require a display to run
# Use headless display adapter or software solution

Automation and Scripting

Remote Commands via ARD

# Run command on remote Mac (requires ARD or kickstart configured)
# This is done from ARD client, not command line on remote

# Alternative: SSH with tmux/screen for persistent sessions
$ ssh user@mac -t "tmux attach || tmux new"

Automating Screen Sharing Connections

#!/bin/bash
# connect-vnc.sh - Connect to VNC with SSH tunnel

HOST=${1:-"mac-remote"}
VNC_PORT=${2:-5901}

# Kill any existing tunnel
pkill -f "ssh.*-L.*:5900"

# Create new tunnel
ssh -f -N -L "$VNC_PORT:localhost:5900" "$HOST"

# Wait for tunnel
sleep 1

# Connect
open "vnc://localhost:$VNC_PORT"

Checking Remote Mac Status

#!/bin/bash
# check-remote-mac.sh - Check if remote Mac is accessible

HOST=${1:-"mac-remote"}

echo "Checking $HOST..."

# Check SSH
if ssh -o ConnectTimeout=5 "$HOST" "echo 'SSH: OK'" 2>/dev/null; then
    echo "SSH: Accessible"
else
    echo "SSH: Not accessible"
    exit 1
fi

# Check Screen Sharing
if ssh "$HOST" "lsof -i :5900 | grep -q screensharing" 2>/dev/null; then
    echo "Screen Sharing: Running"
else
    echo "Screen Sharing: Not running"
fi

# Check user sessions
echo "Logged in users:"
ssh "$HOST" "who"

Quick Reference

Enabling Remote Access

MethodEnable Command
SSHsudo systemsetup -setremotelogin on
Screen SharingSystem Settings → Sharing → Screen Sharing
Remote Managementkickstart -activate -configure -allowAccessFor -allUsers -privs -all

Connecting

FromToMethod
macOSmacOSopen vnc://host.local
macOSLinuxVNC client + SSH tunnel
macOSWindowsMicrosoft Remote Desktop
AnymacOS (internet)SSH tunnel + VNC

Ports

ServicePort
VNC5900 (display :0), 5901 (:1), etc.
ARD3283
RDP3389
SSH22

Summary

Remote access options depend on your use case:

Use CaseRecommended Approach
Mac to Mac (local)Built-in Screen Sharing
Mac to Mac (internet)SSH tunnel + Screen Sharing
Mac to LinuxSSH + VNC through tunnel
Mac to WindowsMicrosoft Remote Desktop
Headless Mac adminSSH + Remote Management
Run Linux GUI appsSSH with X11 forwarding
Easy internet accessTailscale + Screen Sharing

Key security practices:

  1. Never expose VNC directly to the internet
  2. Always use SSH tunnels for remote VNC
  3. Consider VPN solutions like Tailscale for easier secure access
  4. Use strong authentication
  5. Limit which users can access screen sharing

Working with NFS and SMB

Network file sharing is essential for development teams and heterogeneous environments. macOS supports both NFS (Network File System, common in Unix environments) and SMB (Server Message Block, the Windows/Samba protocol). Understanding how to mount, configure, and troubleshoot these protocols helps you work seamlessly across macOS, Linux, and Windows systems.

Protocol Comparison

┌─────────────────────────────────────────────────────────────────┐
│                    Network Filesystem Comparison                 │
├─────────────────────┬──────────────────────┬────────────────────┤
│        NFS          │         SMB          │        AFP         │
├─────────────────────┼──────────────────────┼────────────────────┤
│ Unix-native         │ Windows-native       │ Apple-native       │
│ Fast on Unix        │ Universal support    │ Legacy (deprecated)│
│ UID/GID mapping     │ Username/password    │ Deprecated         │
│ Ports 111, 2049     │ Port 445             │ Port 548           │
│ Stateless (v3)      │ Stateful             │ Stateful           │
│ Best for Unix-Unix  │ Best for mixed env   │ Avoid for new use  │
└─────────────────────┴──────────────────────┴────────────────────┘

SMB: The Universal Protocol

SMB is the most compatible choice for mixed environments. macOS, Windows, and Linux (via Samba) all support it.

Connecting to SMB Shares

Using Finder:

# Go → Connect to Server (Cmd+K)
# Enter: smb://server/share
# Or: smb://username@server/share

# For Windows servers:
# smb://DOMAIN;username@server/share

Using command line:

# Mount SMB share
$ mount_smbfs //user@server/share /Volumes/share

# With domain
$ mount_smbfs //DOMAIN\;user@server/share /Volumes/share

# With specific options
$ mount_smbfs -o nobrowse //user@server/share /Volumes/share

# Create mount point first
$ mkdir -p /Volumes/myshare
$ mount_smbfs //user@server/share /Volumes/myshare

SMB Mount Options

# Common mount options
$ mount_smbfs -o option1,option2 //user@server/share /mount/point

# Available options:
# -N            Don't prompt for password (use keychain)
# -o nobrowse   Hide from Finder sidebar
# -o soft       Soft mount (operations can fail)
# -o nodev      Don't interpret device files
# -o nosuid     Ignore setuid bits

Storing SMB Credentials

# Store credentials in Keychain (happens automatically via Finder)

# Or use security command
$ security add-internet-password \
    -a "username" \
    -s "server.example.com" \
    -w "password" \
    -D "SMB" \
    -T /System/Library/Extensions/smbfs.kext/Contents/Resources/NetAuthSysAgent

# Find stored credentials
$ security find-internet-password -s "server.example.com"

Troubleshooting SMB

# Check SMB version being used
$ smbutil statshares -a

# List shares on server
$ smbutil view //user@server
Share                                           Type
----------------------------------------------
public                                          Disk
admin$                                          Disk
...

# Check connection
$ smbutil lookup server.local

# Force SMB version (if having compatibility issues)
# Create /etc/nsmb.conf
$ sudo tee /etc/nsmb.conf << 'EOF'
[default]
protocol_vers_map=4  # Force SMB2+
signing_required=no
EOF

# Protocol values:
# 7 = SMB1, SMB2, SMB3
# 6 = SMB2, SMB3
# 4 = SMB2 only
# 2 = SMB3 only

SMB Performance Tuning

# /etc/nsmb.conf for performance
$ sudo tee /etc/nsmb.conf << 'EOF'
[default]
# Disable signing for performance (less secure)
signing_required=no
validate_neg_off=yes

# Connection timeout
conn_timeout=10

# Use SMB2/3 only (more efficient)
protocol_vers_map=6

# Disable notifications (reduces overhead)
notify_off=yes
EOF

# Per-server configuration
$ sudo tee -a /etc/nsmb.conf << 'EOF'
[fast-server]
streams=yes

[legacy-server]
protocol_vers_map=7  # Include SMB1 for old servers
EOF

NFS: Native Unix File Sharing

NFS provides better performance and Unix compatibility than SMB when sharing between Unix-like systems.

Mounting NFS Shares

# Basic NFS mount
$ sudo mount -t nfs server:/export/share /Volumes/share

# With options
$ sudo mount -t nfs -o resvport,rw server:/export/share /Volumes/share

# NFSv4 (macOS 10.15+)
$ sudo mount -t nfs -o vers=4 server:/export /Volumes/share

# NFSv3 (more compatible)
$ sudo mount -t nfs -o vers=3,resvport server:/export/share /Volumes/share

NFS Mount Options

# Common options for macOS NFS client
-o resvport    # Use privileged port (often required)
-o rw          # Read-write (default)
-o ro          # Read-only
-o soft        # Soft mount (operations can fail)
-o hard        # Hard mount (retry forever)
-o intr        # Allow interrupt
-o bg          # Background mount
-o rsize=32768 # Read buffer size
-o wsize=32768 # Write buffer size
-o tcp         # Use TCP (default)
-o udp         # Use UDP
-o vers=3      # NFS version 3
-o vers=4      # NFS version 4
-o nolock      # Disable file locking
-o locallocks  # Use local locking only

# Example with performance options
$ sudo mount -t nfs -o resvport,rw,rsize=65536,wsize=65536,soft,intr \
    server:/export /Volumes/nfsmount

Setting Up NFS Exports on macOS

macOS can also serve NFS shares:

# Edit /etc/exports
$ sudo vim /etc/exports

# Export home directory to specific subnet
/Users/developer -mapall=developer -network 192.168.1.0 -mask 255.255.255.0

# Export with specific options
/Volumes/Data -ro -alldirs -network 192.168.1.0 -mask 255.255.255.0

# Export format:
# path [options] [-network ip -mask mask | host...]

# Start NFS server
$ sudo nfsd enable
$ sudo nfsd start

# Check NFS server status
$ sudo nfsd status
nfsd service is enabled
nfsd is running (pid 1234, 8 threads)

# Check exports
$ showmount -e
Exports list on localhost:
/Users/developer                        192.168.1.0

# Reload exports after editing
$ sudo nfsd update

NFS Exports Options

OptionDescription
-roRead-only export
-rwRead-write (rarely needed, it’s default)
-alldirsAllow mounting any subdirectory
-maproot=userMap root to specified user
-mapall=userMap all users to specified user
-networkAllowed network
-maskNetwork mask

Troubleshooting NFS

# Check if NFS server is reachable
$ showmount -e server
Exports list on server:
/export/share         192.168.1.0/24

# Check RPC services
$ rpcinfo -p server

# Test mount (verbose)
$ sudo mount -t nfs -v server:/share /mnt
mount_nfs: /mnt: mounted over server:/share

# Common issues:

# "Operation not permitted"
# Solution: Add -o resvport
$ sudo mount -t nfs -o resvport server:/share /mnt

# "Access denied"
# Check: Server exports, network restrictions

# "No such file or directory"
# Check: Export path exists on server

# Stale file handle
$ sudo umount -f /mnt  # Force unmount
$ sudo mount ...       # Remount

Autofs: Automatic Mounting

Autofs mounts filesystems on-demand and unmounts them after inactivity.

How Autofs Works

┌─────────────────────────────────────────────────────────────────┐
│                      Autofs Flow                                 │
│                                                                  │
│  1. Access /Network/Servers or configured path                  │
│           ↓                                                      │
│  2. automountd receives trigger                                  │
│           ↓                                                      │
│  3. Looks up mount info from auto_master/auto_* maps            │
│           ↓                                                      │
│  4. Mounts filesystem                                            │
│           ↓                                                      │
│  5. After timeout (default 60s), unmounts if idle               │
└─────────────────────────────────────────────────────────────────┘

Autofs Configuration Files

# Main configuration
/etc/auto_master     # Master map, defines mount points

# Map files
/etc/auto_home       # Home directory mounts
/etc/auto_nfs        # NFS mounts
/etc/auto_smb        # SMB mounts (custom)

# View current automounts
$ automount -vc

Configuring Auto-mounted NFS Shares

# /etc/auto_master - add a mount point
$ sudo vim /etc/auto_master

# Add line:
/mnt/nfs    auto_nfs    -nosuid,nodev

# Create /etc/auto_nfs
$ sudo vim /etc/auto_nfs

# Format: key [options] location
projects    -fstype=nfs,resvport,rw    server:/export/projects
data        -fstype=nfs,resvport,ro    server:/export/data
backup      -fstype=nfs,resvport,rw    nas:/backup

# Reload automount
$ sudo automount -vc
automount: /mnt/nfs updated

Configuring Auto-mounted SMB Shares

# /etc/auto_master
/mnt/smb    auto_smb    -nosuid,nodev

# /etc/auto_smb
$ sudo vim /etc/auto_smb

# SMB automounts
shared      -fstype=smbfs    ://user:password@server/share
documents   -fstype=smbfs    ://DOMAIN\;user@server/docs

# Note: Storing passwords in plain text is insecure
# Better: Use Keychain credentials

# Alternative: Use URL with no password (prompts or uses Keychain)
documents   -fstype=smbfs    ://user@server/docs

Testing Autofs

# Reload configuration
$ sudo automount -vc

# Trigger mount by accessing
$ ls /mnt/nfs/projects
# Automount happens automatically

# Check what's mounted
$ mount | grep auto
map auto_nfs on /mnt/nfs (autofs, automounted, nobrowse)

# View automount activity
$ sudo automount -fvc

Autofs on Login Items

For mounts that should be available at login:

# Create a script that triggers autofs mounts
#!/bin/bash
# ~/bin/mount-shares.sh
ls /mnt/nfs/projects > /dev/null 2>&1
ls /mnt/smb/shared > /dev/null 2>&1

# Add to login items or launchd agent

Permissions and User Mapping

The UID/GID Challenge

Different systems may use different user IDs:

┌─────────────────────────────────────────────────────────────────┐
│                    UID/GID Mapping Issues                        │
│                                                                  │
│  macOS:  user "david" has UID 501                               │
│  Linux:  user "david" has UID 1000                              │
│                                                                  │
│  Files created on Linux with UID 1000 appear as                 │
│  "unknown user" on macOS, and vice versa.                       │
└─────────────────────────────────────────────────────────────────┘

NFS User Mapping

# On macOS NFS server - map all access to specific user
/Users/shared -mapall=501:20 -network 192.168.1.0 -mask 255.255.255.0

# -mapall=UID:GID maps all remote users to specified UID/GID

# On Linux NFS server (/etc/exports):
/export/shared 192.168.1.0/24(rw,sync,all_squash,anonuid=1000,anongid=1000)

# all_squash: Maps all users to anonymous
# anonuid/anongid: UID/GID for anonymous

SMB User Mapping

SMB uses username/password authentication, avoiding UID issues:

# SMB maps users based on credentials
# Files are owned by the authenticated user

# Force file ownership (macOS mount option)
$ mount_smbfs -o uid=501,gid=20 //user@server/share /Volumes/share

Persistent Mounts

Using /etc/fstab (Limited on macOS)

macOS supports /etc/fstab but with limitations:

# /etc/fstab format
# device    mount_point    type    options    dump    pass

# NFS mount
server:/export/share    /mnt/nfs    nfs    resvport,rw,bg    0    0

# Note: macOS fstab support is limited
# Autofs or login scripts are often better

Using Login Items

For user-specific mounts:

# Create mount script
$ cat > ~/bin/mount-shares.sh << 'EOF'
#!/bin/bash
# Mount work shares

# NFS
if ! mount | grep -q "/Volumes/work-nfs"; then
    mkdir -p /Volumes/work-nfs
    mount -t nfs -o resvport server:/export/work /Volumes/work-nfs
fi

# SMB (will use Keychain credentials)
if ! mount | grep -q "/Volumes/work-smb"; then
    mkdir -p /Volumes/work-smb
    mount_smbfs //user@server/share /Volumes/work-smb
fi
EOF

$ chmod +x ~/bin/mount-shares.sh

# Add to System Settings → Login Items
# Or create a launchd agent

Using launchd for Mounting

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.mountshares</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/user/bin/mount-shares.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StartInterval</key>
    <integer>300</integer>
</dict>
</plist>
# Save to ~/Library/LaunchAgents/com.user.mountshares.plist
$ launchctl load ~/Library/LaunchAgents/com.user.mountshares.plist

Performance Optimization

NFS Performance

# Larger read/write buffers
$ sudo mount -t nfs -o resvport,rsize=65536,wsize=65536 server:/share /mnt

# Use TCP (default, but explicit)
$ sudo mount -t nfs -o resvport,tcp server:/share /mnt

# Disable attribute caching for consistency (slower but more accurate)
$ sudo mount -t nfs -o resvport,noac server:/share /mnt

# Enable attribute caching for speed (default)
$ sudo mount -t nfs -o resvport,ac server:/share /mnt

SMB Performance

# /etc/nsmb.conf optimizations
[default]
# Disable signing (significant performance impact)
signing_required=no

# Disable opportunistic locks if causing issues
dir_cache_max_cnt=0

# Increase timeout for slow networks
conn_timeout=30

# Disable change notifications
notify_off=yes

Testing Transfer Speeds

# Test write speed
$ dd if=/dev/zero of=/Volumes/share/testfile bs=1m count=1000
1000+0 records in
1000+0 records out
1048576000 bytes transferred in 12.345 secs (84.9 MB/sec)

# Test read speed
$ dd if=/Volumes/share/testfile of=/dev/null bs=1m
1000+0 records in
1000+0 records out
1048576000 bytes transferred in 8.765 secs (119.6 MB/sec)

# Clean up
$ rm /Volumes/share/testfile

Common Issues and Solutions

“Operation not permitted” (NFS)

# macOS requires privileged ports for NFS
$ sudo mount -t nfs -o resvport server:/share /mnt

“Permission denied” (SMB)

# Check credentials in Keychain
$ security find-internet-password -s "server"

# Try explicit username
$ mount_smbfs //DOMAIN\;username@server/share /mnt

# Reset Keychain entry
$ security delete-internet-password -s "server"
# Then remount (will prompt for password)

Slow Performance

# For NFS
# - Increase buffer sizes: rsize=65536,wsize=65536
# - Check network path: traceroute server
# - Test raw network speed: iperf3

# For SMB
# - Disable signing in /etc/nsmb.conf
# - Use SMB3 if available: protocol_vers_map=2
# - Check for packet loss: ping -c 100 server

Mount Becomes Unresponsive

# Force unmount
$ sudo umount -f /Volumes/share

# If that fails, kill processes using mount
$ lsof +D /Volumes/share
# Kill listed processes
$ sudo umount -f /Volumes/share

# Last resort: lazy unmount
$ sudo diskutil unmount force /Volumes/share

Files Show Wrong Ownership

# For NFS, configure user mapping on server
# For SMB, use uid/gid mount options:
$ mount_smbfs -o uid=$(id -u),gid=$(id -g) //user@server/share /mnt

Quick Reference

Mounting Shares

ProtocolCommand
SMBmount_smbfs //user@server/share /mnt
NFSsudo mount -t nfs -o resvport server:/share /mnt
Finderopen smb://server/share or open nfs://server/share

Common Mount Options

ProtocolOptionDescription
NFSresvportUse privileged port (required for most servers)
NFSvers=3 or vers=4NFS version
NFSsoftAllow operations to fail
SMBnobrowseHide from Finder sidebar
BothroRead-only

Configuration Files

FilePurpose
/etc/nsmb.confSMB client configuration
/etc/exportsNFS server exports
/etc/auto_masterAutofs master map
/etc/auto_*Autofs mount maps
/etc/fstabStatic mounts (limited use on macOS)

Summary

Choosing between NFS and SMB:

Use CaseRecommended
Mac ↔ MacSMB (default) or NFS
Mac ↔ LinuxNFS (if pure Unix) or SMB (if mixed)
Mac ↔ WindowsSMB
Mixed environmentSMB (most compatible)
Performance critical (Unix)NFS
Simple setupSMB (Finder support)

Key practices:

  1. Use autofs for on-demand mounting
  2. Store SMB credentials in Keychain
  3. Use resvport option for NFS on macOS
  4. Configure user mapping for NFS to avoid ownership issues
  5. Tune SMB settings in /etc/nsmb.conf for performance
  6. Always use force unmount (umount -f) if mount becomes stuck

Performance and Optimization

Understanding and optimizing macOS performance requires knowledge of both traditional Unix performance concepts and Apple-specific technologies. macOS combines the robust process and memory management of BSD with Apple’s innovations in power efficiency, storage optimization, and graphics performance. This part covers the tools and techniques for monitoring, analyzing, and improving system performance.

The Performance Landscape

macOS performance optimization spans multiple layers:

┌─────────────────────────────────────────────────────────────────┐
│                    Application Layer                             │
│          (CPU usage, memory footprint, I/O patterns)            │
├─────────────────────────────────────────────────────────────────┤
│                    Framework Layer                               │
│        (Grand Central Dispatch, Metal, Core Animation)          │
├─────────────────────────────────────────────────────────────────┤
│                    System Services                               │
│         (WindowServer, launchd, mds, kernel_task)               │
├─────────────────────────────────────────────────────────────────┤
│                    Kernel / XNU                                  │
│      (Memory management, scheduler, I/O subsystem)              │
├─────────────────────────────────────────────────────────────────┤
│                    Hardware                                      │
│     (CPU cores, unified memory, SSD, Neural Engine)             │
└─────────────────────────────────────────────────────────────────┘

Each layer has its own characteristics and optimization strategies.

Key Performance Metrics

CPU Performance

macOS uses a sophisticated scheduler that manages:

ConceptDescription
Efficiency Cores (E-cores)Lower power, background tasks (Apple Silicon)
Performance Cores (P-cores)Maximum throughput (Apple Silicon)
Quality of Service (QoS)Thread priority classification
CPU ThrottlingThermal and power management
# Quick CPU overview
$ sysctl -n hw.ncpu
8

# Apple Silicon core types
$ sysctl hw.perflevel0.physicalcpu hw.perflevel1.physicalcpu
hw.perflevel0.physicalcpu: 4   # Performance cores
hw.perflevel1.physicalcpu: 4   # Efficiency cores

# Current CPU usage
$ top -l 1 -n 0 | grep "CPU usage"
CPU usage: 5.26% user, 3.50% sys, 91.23% idle

Memory Performance

macOS memory management includes unique features:

FeaturePurpose
Unified MemoryShared CPU/GPU memory (Apple Silicon)
Memory CompressionCompress inactive pages instead of swapping
App NapReduce memory/CPU for background apps
Memory PressureSystem-wide memory demand indicator
# Memory overview
$ vm_stat
Pages free:                               45231.
Pages active:                            892341.
Pages inactive:                          234521.
Pages speculative:                        12345.
Pages wired down:                        456789.
Pages compressed:                        567890.

# Pressure level
$ memory_pressure
System-wide memory free percentage: 42%

Storage Performance

APFS and modern SSDs require different optimization approaches:

FactorImpact
SSD TrimMaintains write performance
APFS SnapshotsCan consume space
Spotlight IndexingBackground I/O during indexing
Time MachinePeriodic backup I/O
# Disk I/O statistics
$ iostat -d 1 3
              disk0
    KB/t  tps  MB/s
   24.00   45  1.05

# APFS container space
$ diskutil apfs list

Power Performance

Especially important on laptops:

AspectConsideration
CPU FrequencyDynamic scaling based on demand
Display BrightnessMajor power consumer
Discrete GPUSignificant power draw when active
Background ActivityApps preventing sleep
# Power state
$ pmset -g
System-wide power settings:
 SleepDisabled          0
Currently in use:
 hibernatemode        3
 powernap             1
 sleep                1

Quick System Health Check

A fast assessment of overall system performance:

#!/bin/bash
# Quick system health check

echo "=== CPU ==="
top -l 1 -n 0 | grep "CPU usage"

echo -e "\n=== Memory ==="
memory_pressure 2>/dev/null || vm_stat | head -10

echo -e "\n=== Disk ==="
df -h / | tail -1

echo -e "\n=== Load Average ==="
uptime

echo -e "\n=== Top Processes by CPU ==="
ps aux | sort -nrk 3 | head -6

echo -e "\n=== Top Processes by Memory ==="
ps aux | sort -nrk 4 | head -6

GUI vs CLI Performance Tools

macOS provides both graphical and command-line performance tools:

TaskGUI ToolCLI Tool
Process monitoringActivity Monitortop, htop, ps
Memory analysisActivity Monitorvm_stat, memory_pressure
Disk I/OActivity Monitoriostat, fs_usage
NetworkActivity Monitornettop, netstat
CPU profilingInstrumentssample, spindump
EnergyActivity Monitorpowermetrics
System traceInstrumentsfs_usage, sc_usage

Performance on Apple Silicon vs Intel

Apple Silicon Macs have fundamentally different performance characteristics:

AspectIntel MacApple Silicon Mac
MemoryDedicated RAMUnified Memory (shared CPU/GPU)
CoresSymmetricAsymmetric (P-cores + E-cores)
Power StatesIntel SpeedStepApple custom (more granular)
Thermal DesignOften throttles under loadMore consistent performance
GPUDiscrete or integratedIntegrated, shares memory
Rosetta 2N/A~80-90% native performance
# Check architecture
$ uname -m
arm64  # Apple Silicon
x86_64 # Intel

# Check if running under Rosetta
$ sysctl sysctl.proc_translated
sysctl.proc_translated: 0  # Native
sysctl.proc_translated: 1  # Rosetta

What You’ll Learn in This Part

Activity Monitor: Beyond the GUI explores Activity Monitor’s tabs in depth, hidden columns, what each metric means, and how to export data for analysis.

Command-Line Performance Tools covers essential CLI tools: top, htop, vm_stat, iostat, fs_usage, powermetrics, and sample, with practical examples and interpretation guides.

Intel vs Apple Silicon Considerations explains performance differences between architectures, Rosetta 2 overhead, universal binaries, and optimizing for each platform.

Memory Management Deep Dive examines how macOS manages memory, including compression, swap, memory pressure, diagnosing leaks, and optimizing memory usage.

Disk I/O Optimization covers measuring and improving storage performance, APFS optimizations, SSD health, and identifying I/O bottlenecks.

Power Management and Battery explains pmset, caffeinate, power assertions, App Nap, and strategies for maximizing battery life.

Troubleshooting Performance Issues provides a systematic approach to diagnosing slowdowns, including common causes, diagnostic workflows, and remediation strategies.

Common Performance Tasks

Find What’s Using CPU

# Interactive view
$ top -o cpu

# Snapshot of top CPU consumers
$ ps aux | sort -nrk 3 | head -10

# Sample a process for analysis
$ sudo sample PID 5 -file /tmp/sample.txt

Find What’s Using Memory

# Sort by memory in top
$ top -o mem

# Detailed memory stats
$ vm_stat

# Memory pressure
$ memory_pressure

Find What’s Using Disk

# Real-time I/O monitoring (requires Full Disk Access)
$ sudo fs_usage -f filesys

# I/O statistics
$ iostat -d 2

Check System Responsiveness

# Load average
$ uptime
10:30  up 5 days,  3:45, 3 users, load averages: 1.25 2.30 2.15

# Interpretation:
# 1-minute: 1.25 (recent load)
# 5-minute: 2.30 (short-term trend)
# 15-minute: 2.15 (longer-term trend)

# Compare to CPU count for utilization
$ sysctl -n hw.ncpu
8
# Load of 8.0 = 100% utilization on 8 cores

Monitor Power Usage

# Detailed power metrics (requires root)
$ sudo powermetrics --samplers cpu_power -n 1

# Battery status
$ pmset -g batt
Now drawing from 'Battery Power'
 -InternalBattery-0 (id=1234567) 85%; discharging; 4:30 remaining

Performance Best Practices

General Guidelines

  1. Monitor before optimizing: Establish baseline measurements
  2. Focus on bottlenecks: Optimize the limiting factor first
  3. Consider power impact: Performance gains may cost battery life
  4. Test on target hardware: Performance varies by Mac model
  5. Use native builds: Avoid Rosetta 2 overhead when possible

Quick Wins

# Free up disk space (clears caches)
$ sudo purge

# Rebuild Spotlight index if mds is consuming resources
$ sudo mdutil -E /

# Clear DNS cache if network is slow
$ sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder

# Disable Spotlight for a volume (development drives)
$ sudo mdutil -i off /Volumes/ExternalDrive

# Check for runaway processes
$ top -l 1 -o cpu -n 5

When to Investigate

Investigate performance when you observe:

  • High CPU: Fan running, system warm, UI lag
  • High Memory: Memory pressure warnings, swap usage
  • High Disk I/O: Spinning cursor, slow app launches
  • High Energy: Battery draining faster than expected
  • High Network: Unexpected data usage, slow downloads

The following chapters provide detailed coverage of each performance domain, with practical diagnostic commands and optimization strategies.

Activity Monitor: Beyond the GUI

Activity Monitor is macOS’s built-in system monitor, but most users only scratch the surface of its capabilities. This chapter explores every tab, hidden columns, data interpretation, and techniques for extracting actionable performance insights.

Launching Activity Monitor

# Open from Terminal
$ open -a "Activity Monitor"

# Or via Spotlight (Cmd+Space, type "Activity Monitor")

Activity Monitor can also be controlled via AppleScript for automation:

# Get process list via AppleScript
$ osascript -e 'tell application "System Events" to get name of every process'

The Five Tabs

Activity Monitor organizes system information into five tabs:

┌─────────────────────────────────────────────────────────────────┐
│  CPU  │  Memory  │  Energy  │  Disk  │  Network  │             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│                    Process List                                 │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│                    Bottom Panel                                 │
│              (Tab-specific graphs/stats)                        │
└─────────────────────────────────────────────────────────────────┘

Each tab shows different columns and bottom panel information.

CPU Tab

Default Columns

ColumnDescription
Process NameApplication or process name
% CPUPercentage of CPU time used
CPU TimeTotal CPU time consumed
ThreadsNumber of active threads
Idle Wake UpsTimes process woke from idle
PIDProcess identifier
UserUsername owning the process

Hidden CPU Columns

Right-click the column header to add these valuable hidden columns:

ColumnDescriptionWhen Useful
% GPUGPU utilizationGraphics/video work
GPU TimeTotal GPU timeIdentifying GPU hogs
Architecturearm64/x86_64Finding Rosetta processes
SandboxSandboxed statusSecurity analysis
RestrictedHardened runtimeSecurity analysis
App NapApp Nap statusEnergy debugging
Sudden TerminationCan be killed safelyShutdown debugging
Preventing SleepBlocking system sleepBattery debugging

Bottom Panel: CPU Usage

┌─────────────────────────────────────────────────────────────────┐
│ System: 5.50%  ████▌                                            │
│ User:   12.25% ████████████▎                                    │
│ Idle:   82.25% ██████████████████████████████████████████████  │
├─────────────────────────────────────────────────────────────────┤
│ Threads: 1,234    Processes: 456                                │
└─────────────────────────────────────────────────────────────────┘

Understanding CPU percentages:

  • System: Kernel and system service work
  • User: User application work
  • Idle: Available CPU capacity

On multi-core systems, a single process can exceed 100% (e.g., 400% = 4 cores fully utilized).

CPU Interpretation

# Equivalent CLI information
$ top -l 1 -n 0 | grep -E "CPU|Processes|Threads"
Processes: 456 total, 3 running, 453 sleeping, 1234 threads
CPU usage: 12.25% user, 5.50% sys, 82.25% idle

Warning signs:

  • System % consistently > 20%: Possible driver or kernel issue
  • User % near 100%: CPU-bound application
  • High idle wake ups: Power efficiency problem

Memory Tab

Default Columns

ColumnDescription
MemoryCurrent memory footprint
Real MemoryPhysical RAM used
Virtual MemoryAddress space size
Shared MemoryMemory shared with other processes
Real Private MemoryNon-shared physical RAM
Compressed MemoryCompressed pages in RAM

Hidden Memory Columns

ColumnDescriptionWhen Useful
Purgeable MemoryMemory that can be reclaimedMemory optimization
Real Shared MemoryActually shared RAMLibrary sharing analysis
Dirty MemoryModified pagesSwap prediction
Swapped MemoryMemory paged to diskPerformance issues

Bottom Panel: Memory Pressure

┌─────────────────────────────────────────────────────────────────┐
│ Memory Pressure:                                                │
│ [████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]            │
│                    Green (Normal)                               │
├─────────────────────────────────────────────────────────────────┤
│ Physical Memory:     16.00 GB                                   │
│ Memory Used:         12.45 GB                                   │
│   App Memory:        8.23 GB                                    │
│   Wired Memory:      2.12 GB                                    │
│   Compressed:        2.10 GB                                    │
│ Cached Files:        2.34 GB                                    │
│ Swap Used:           0 bytes                                    │
└─────────────────────────────────────────────────────────────────┘

Memory Pressure Colors:

ColorMeaningAction
GreenMemory availableNormal operation
YellowMemory becoming limitedConsider closing apps
RedMemory critically lowClose apps, investigate

Memory Categories:

CategoryDescription
App MemoryMemory actively used by applications
Wired MemoryKernel memory, cannot be compressed/swapped
CompressedInactive pages compressed in RAM
Cached FilesFile data cached for faster access

Memory Interpretation

# Equivalent CLI information
$ vm_stat
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free:                               45231.
Pages active:                            892341.
Pages inactive:                          234521.
Pages wired down:                        456789.
Pages compressed:                        567890.
...

# Memory pressure
$ memory_pressure
System-wide memory free percentage: 42%
System memory pressure level: 1 (normal)

Warning signs:

  • Memory pressure yellow/red: System is memory constrained
  • Swap used > 0: RAM exhausted, performance will degrade
  • Compressed memory very high: Near memory limits

Energy Tab

Default Columns

ColumnDescription
Energy ImpactRelative power consumption
Avg Energy ImpactAverage over last 8 hours
App NapIs App Nap active
Preventing SleepBlocking system sleep
Graphics CardWhich GPU in use

Hidden Energy Columns

ColumnDescriptionWhen Useful
PowerInstantaneous power drawBattery debugging
Requires High Perf GPUNeeds discrete GPUGPU switching
GPU ActivityGPU busy percentageGraphics work

Bottom Panel: Energy Impact

┌─────────────────────────────────────────────────────────────────┐
│ Energy Impact:                                                  │
│ [History graph showing energy over time]                        │
├─────────────────────────────────────────────────────────────────┤
│ Battery Level: 85%                                              │
│ Time on Battery: 2:30                                           │
│ Time Remaining: 4:00                                            │
│ Graphics Card: Integrated (Intel/Apple)                         │
│ Battery (Last 12 hours): Apps Using Significant Energy:         │
│   Chrome, Slack, Xcode                                          │
└─────────────────────────────────────────────────────────────────┘

Energy Impact Interpretation

Energy Impact is a relative score, not watts:

ScoreImpactExample
0-4LowText editors, system utilities
4-12MediumBrowsers (idle), communication apps
12-30HighVideo playback, compilation
30+Very HighGaming, video rendering
# CLI energy information
$ pmset -g batt
Now drawing from 'Battery Power'
 -InternalBattery-0 (id=1234567) 85%; discharging; 4:00 remaining

# Detailed power metrics
$ sudo powermetrics --samplers cpu_power,tasks -n 1

Warning signs:

  • “Preventing Sleep” apps: May drain battery unexpectedly
  • High “Avg Energy Impact” apps: Consistently power-hungry
  • Discrete GPU active: Significant power draw

Disk Tab

Default Columns

ColumnDescription
Bytes ReadTotal bytes read from disk
Bytes WrittenTotal bytes written to disk
Reads InNumber of read operations
Writes OutNumber of write operations

Hidden Disk Columns

ColumnDescriptionWhen Useful
Bytes Read/secRead throughputI/O bottleneck detection
Bytes Written/secWrite throughputI/O bottleneck detection
Read DeltaRecent readsActive I/O identification
Write DeltaRecent writesActive I/O identification

Bottom Panel: Disk Activity

┌─────────────────────────────────────────────────────────────────┐
│ Disk Activity:                                                  │
│ [Read/Write graph over time]                                    │
├─────────────────────────────────────────────────────────────────┤
│ Data read:     1.25 GB        Data read/sec:     15 MB/s       │
│ Data written:  856 MB         Data written/sec:  5 MB/s        │
│ Reads in:      45,678         Writes out:        23,456        │
└─────────────────────────────────────────────────────────────────┘

Disk Interpretation

# CLI disk I/O
$ iostat -d 2
              disk0
    KB/t  tps  MB/s
   24.00   45  1.05

# Per-process I/O (requires Full Disk Access)
$ sudo iotop

Warning signs:

  • Constant high writes: May indicate logging issue or memory pressure
  • Spiky reads: Spotlight indexing or Time Machine
  • High I/O with low CPU: I/O bound workload

Network Tab

Default Columns

ColumnDescription
Sent BytesTotal bytes transmitted
Received BytesTotal bytes received
Sent PacketsNumber of packets sent
Received PacketsNumber of packets received

Hidden Network Columns

ColumnDescriptionWhen Useful
Sent Bytes/secUpload rateBandwidth monitoring
Received Bytes/secDownload rateBandwidth monitoring
Sent Packets/secPacket rateNetwork debugging
Received Packets/secPacket rateNetwork debugging

Bottom Panel: Network Activity

┌─────────────────────────────────────────────────────────────────┐
│ Network Activity:                                               │
│ [Send/Receive graph over time]                                  │
├─────────────────────────────────────────────────────────────────┤
│ Data received:  2.5 GB       Data received/sec:  1.2 MB/s      │
│ Data sent:      450 MB       Data sent/sec:      250 KB/s      │
│ Packets in:     1,234,567    Packets out:        456,789       │
└─────────────────────────────────────────────────────────────────┘

Network Interpretation

# CLI network statistics
$ nettop -P -L 1

# Network connections per process
$ lsof -i -P | head -20

Warning signs:

  • Unexpected high bandwidth: Possible malware or sync issues
  • Unknown processes with network activity: Security concern
  • High packet rate with low data: Possible DoS or scan

Advanced Features

View All Processes

By default, Activity Monitor shows only your processes:

  1. View menu > All Processes
  2. Or View > All Processes, Hierarchically (shows parent-child)
# CLI equivalent
$ ps aux | wc -l      # All processes
$ ps -u $(whoami)     # Only your processes

Process Hierarchy View

View > All Processes, Hierarchically shows:

▼ launchd (1)
   ▼ UserEventAgent (234)
   ▼ Dock (456)
   ▼ Finder (789)
   ▼ loginwindow (123)
      ▼ Terminal (345)
         ▼ zsh (567)
            ▼ top (890)

Inspect Process

Double-click any process to see detailed information:

Open Files and Ports tab:

  • File descriptors
  • Network connections
  • Shows what resources the process uses
# CLI equivalent
$ lsof -p PID

Memory tab:

  • Memory regions
  • Memory map
  • Detailed memory breakdown
# CLI equivalent
$ vmmap PID

Statistics tab:

  • CPU usage history
  • Context switches
  • Page faults

Sampling tab:

  • Takes a performance sample
  • Shows where CPU time is spent
# CLI equivalent
$ sample PID 5 -f /tmp/sample.txt

Diagnostic Reports

Activity Monitor can create system reports:

View menu > System Diagnostic

This runs:

  • sysdiagnose in the background
  • Creates a comprehensive system report
  • Saves to ~/Desktop or specified location
# CLI equivalent
$ sudo sysdiagnose

Spindump

When an app is unresponsive:

  1. Select the process
  2. View > Sample Process or View > Spindump
# CLI equivalent
$ sudo spindump PID 5 -file /tmp/spindump.txt

Exporting Data

Copy Process Information

  1. Select process(es)
  2. Edit > Copy (Cmd+C)

Copies tab-separated data for spreadsheets.

Sample Data Script

Export Activity Monitor-like data programmatically:

#!/bin/bash
# activity-export.sh - Export process data

echo "Timestamp,PID,Process,CPU%,Memory(MB),Threads"
while IFS= read -r line; do
    pid=$(echo "$line" | awk '{print $1}')
    cpu=$(echo "$line" | awk '{print $2}')
    mem=$(echo "$line" | awk '{print $3}')
    name=$(echo "$line" | awk '{for(i=4;i<=NF;i++) printf $i" "; print ""}')
    threads=$(ps -p "$pid" -o nlwp= 2>/dev/null || echo "0")
    echo "$(date +%Y-%m-%d_%H:%M:%S),$pid,$name,$cpu,$mem,$threads"
done < <(ps -eo pid,%cpu,rss,comm | tail -n +2 | sort -k2 -rn | head -20)

Hidden Preferences

Activity Monitor stores preferences in:

$ defaults read com.apple.ActivityMonitor

# Useful settings:
# Show all processes by default
$ defaults write com.apple.ActivityMonitor ShowCategory -int 100

# Update frequency (1=very often, 5=rarely)
$ defaults write com.apple.ActivityMonitor UpdatePeriod -int 2

# Icon type in Dock (0=app icon, 2=CPU history, 3=network, 5=disk, 6=CPU)
$ defaults write com.apple.ActivityMonitor IconType -int 6

Dock Icon Monitoring

Activity Monitor can show live stats in the Dock:

View menu > Dock Icon:

  • Application Icon (default)
  • CPU Usage
  • CPU History
  • Network Usage
  • Disk Activity

This provides at-a-glance system monitoring.

Comparison: Activity Monitor vs CLI Tools

FeatureActivity MonitorCLI Tools
Visual graphsYesNo (unless using htop)
Process hierarchyYespstree, ps -ef
Real-time updatesYestop, htop
Sample processYessample command
Export dataCopy onlyRedirect to file
ScriptableLimitedFully scriptable
Remote accessNoVia SSH
Resource usageHigherLower

Best Practices

For Troubleshooting

  1. Start with the right tab: CPU for slow system, Memory for app crashes
  2. Enable hidden columns: Architecture, Preventing Sleep are invaluable
  3. Use hierarchical view: Find parent processes causing issues
  4. Sample unresponsive apps: Gather data before force quitting

For Monitoring

  1. Set Dock icon to CPU: Quick visual indicator
  2. Keep Activity Monitor running: Catch intermittent issues
  3. Check Memory Pressure regularly: Early warning of problems
  4. Review Energy tab on battery: Identify power hogs

For Analysis

  1. Sort by relevant metric: % CPU for performance, Energy for battery
  2. Watch over time: Patterns reveal issues better than snapshots
  3. Cross-reference tabs: High memory often correlates with disk I/O
  4. Use Inspect window: Deep dive into suspicious processes

Summary

Activity Monitor provides comprehensive system monitoring:

TabKey MetricsWatch For
CPU% CPU, System/User splitRunaway processes, high system %
MemoryMemory Pressure, CompressedYellow/red pressure, swap usage
EnergyEnergy Impact, Preventing SleepBattery drainers, sleep blockers
DiskRead/Write ratesExcessive I/O, constant writes
NetworkSend/Receive ratesUnexpected traffic, high bandwidth

Key hidden columns to enable:

  • Architecture: Identify Rosetta processes
  • Preventing Sleep: Find battery drainers
  • App Nap: Verify power optimization
  • GPU: Track graphics usage

Activity Monitor is excellent for visual monitoring and quick investigations. For scripting, automation, and remote access, the command-line tools covered in the next chapter are essential.

Command-Line Performance Tools

The command line provides powerful performance monitoring tools that work over SSH, can be scripted, and often provide more detail than GUI alternatives. This chapter covers essential CLI tools for monitoring CPU, memory, disk, network, and power performance on macOS.

top - Process Monitor

top is the classic Unix process monitor, available on every macOS system.

Basic Usage

# Start top
$ top

# Non-interactive mode (for scripts)
$ top -l 1

# Show only 10 processes
$ top -l 1 -n 10

# Update every 2 seconds
$ top -s 2

Interactive Commands

While top is running:

KeyAction
qQuit
oChange sort order
OSecondary sort
sChange update interval
UFilter by user
SToggle cumulative mode
pToggle process ID
eToggle task info
?Help

Sorting Options

# Sort by CPU (default)
$ top -o cpu

# Sort by memory
$ top -o mem

# Sort by process ID
$ top -o pid

# Sort by time
$ top -o time

# Sort by threads
$ top -o th

# Available sort keys
$ top -O

Output Interpretation

Processes: 456 total, 3 running, 453 sleeping, 1234 threads
Load Avg: 1.25, 2.30, 2.15
CPU usage: 12.25% user, 5.50% sys, 82.25% idle
SharedLibs: 150M resident, 45M data, 10M linkedit
MemRegions: 78901 total, 3456M resident, 123M private, 890M shared
PhysMem: 14G used (3456M wired, 5678M compressor), 2000M unused
VM: 2345G vsize, 1234M framework vsize, 12345(0) swapins, 23456(0) swapouts
Networks: packets: 1234567/890M in, 456789/123M out
Disks: 123456/4567M read, 78901/2345M written

PID    COMMAND      %CPU TIME     #TH   #WQ  #PORT MEM    PURG   CMPRS  PGRP
12345  Safari       25.0 02:30.45 48    12   456   1234M  45M    567M   12345

Header sections:

SectionMeaning
Load Avg1, 5, 15-minute CPU load averages
CPU usageUser/system/idle breakdown
PhysMemPhysical memory: used, wired, compressed, unused
VMVirtual memory and swap activity
NetworksNetwork I/O summary
DisksDisk I/O summary

Process columns:

ColumnMeaning
%CPUCPU utilization percentage
TIMETotal CPU time consumed
#THThread count
#WQWork queue threads
#PORTMach ports
MEMMemory footprint
PURGPurgeable memory
CMPRSCompressed memory
PGRPProcess group

Scripting with top

# Get CPU usage summary
$ top -l 1 -n 0 | grep "CPU usage"
CPU usage: 12.25% user, 5.50% sys, 82.25% idle

# Get top 5 CPU consumers
$ top -l 1 -n 5 -o cpu -stats pid,command,cpu | tail -6

# Monitor specific process
$ top -pid 12345

# CSV-friendly output
$ top -l 1 -n 10 -stats pid,cpu,mem,command | tail -11

htop - Enhanced Process Monitor

htop provides a more user-friendly interface than top, with color-coded displays and mouse support.

Installation

# Install via Homebrew
$ brew install htop

Features Over top

Featuretophtop
ColorsNoYes
Mouse supportNoYes
Scroll process listLimitedYes
Tree viewNoYes
Kill with keyNoYes
SearchNoYes
FilterLimitedYes
Setup menuNoYes

Interactive Commands

KeyAction
F1 / hHelp
F2 / SSetup menu
F3 / /Search
F4 / \Filter
F5 / tTree view
F6 / >Sort by column
F9 / kKill process
F10 / qQuit
SpaceTag process
uFilter by user
pSort by CPU
MSort by memory
TSort by time

Display Layout

┌─────────────────────────────────────────────────────────────────┐
│ CPU[||||||||      25.0%]  Tasks: 456, 123 thr; 3 running        │
│ CPU[|||           12.5%]  Load average: 1.25 2.30 2.15          │
│ CPU[||||||        18.0%]  Uptime: 5 days, 03:45:30              │
│ Mem[|||||||||||4.5G/16G]                                        │
│ Swp[              0K/0K]                                        │
├─────────────────────────────────────────────────────────────────┤
│   PID USER      PRI  NI  VIRT   RES   SHR S  CPU%  MEM%   TIME+ │
│ 12345 david      20   0 5432M  890M  123M S  25.0  5.6  2:30.45 │
│ 23456 david      20   0 3210M  567M   89M S  12.5  3.5  1:15.22 │
└─────────────────────────────────────────────────────────────────┘

htop on macOS Notes

# Run with sudo for full information
$ sudo htop

# Some features require root:
# - Viewing all process details
# - Killing other users' processes
# - Seeing kernel threads

vm_stat - Virtual Memory Statistics

vm_stat displays Mach virtual memory statistics.

Basic Usage

# One-time snapshot
$ vm_stat

# Continuous monitoring (every 1 second)
$ vm_stat 1

Output Interpretation

$ vm_stat
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free:                               45231.
Pages active:                            892341.
Pages inactive:                          234521.
Pages speculative:                        12345.
Pages throttled:                              0.
Pages wired down:                        456789.
Pages purgeable:                          23456.
"Translation faults":                1234567890.
Pages copy-on-write:                   12345678.
Pages zero filled:                    234567890.
Pages reactivated:                      1234567.
Pages purged:                            234567.
File-backed pages:                       345678.
Anonymous pages:                         567890.
Pages stored in compressor:              890123.
Pages occupied by compressor:            123456.
Decompressions:                          345678.
Compressions:                            567890.
Pageins:                                 123456.
Pageouts:                                     0.
Swapins:                                      0.
Swapouts:                                     0.

Key metrics:

MetricMeaning
Pages freeAvailable memory pages
Pages activeRecently used pages
Pages inactiveNot recently used, can be reclaimed
Pages wired downKernel memory, cannot be paged out
Pages stored in compressorCompressed memory
PageinsPages read from disk
PageoutsPages written to disk (swap)
Swapins/SwapoutsSwap file activity

Converting to Bytes

Page size varies by system. Convert with:

# Get page size
$ pagesize
16384

# Calculate memory values
$ vm_stat | awk -v pagesize=$(pagesize) '
/Pages free/ {printf "Free: %.2f GB\n", $3 * pagesize / 1024/1024/1024}
/Pages active/ {printf "Active: %.2f GB\n", $3 * pagesize / 1024/1024/1024}
/Pages wired/ {printf "Wired: %.2f GB\n", $4 * pagesize / 1024/1024/1024}
'

Monitoring Script

#!/bin/bash
# vm-monitor.sh - Monitor memory stats over time

echo "Time,Free(GB),Active(GB),Wired(GB),Compressed(GB),Swapouts"
while true; do
    stats=$(vm_stat)
    pagesize=$(pagesize)

    free=$(echo "$stats" | awk '/Pages free/ {print $3}' | tr -d '.')
    active=$(echo "$stats" | awk '/Pages active/ {print $3}' | tr -d '.')
    wired=$(echo "$stats" | awk '/Pages wired/ {print $4}' | tr -d '.')
    compressed=$(echo "$stats" | awk '/Pages stored in compressor/ {print $6}' | tr -d '.')
    swapouts=$(echo "$stats" | awk '/Swapouts/ {print $2}' | tr -d '.')

    echo "$(date +%H:%M:%S),$(echo "scale=2; $free * $pagesize / 1073741824" | bc),$(echo "scale=2; $active * $pagesize / 1073741824" | bc),$(echo "scale=2; $wired * $pagesize / 1073741824" | bc),$(echo "scale=2; $compressed * $pagesize / 1073741824" | bc),$swapouts"

    sleep 5
done

memory_pressure - Memory Pressure Assessment

memory_pressure provides a quick memory status check.

$ memory_pressure
System-wide memory free percentage: 42%
System memory pressure level: 1

The system memory pressure level is
currently: 1 (Normal)

Mach zone information: 11345 total zones.
...

Pressure levels:

LevelMeaning
1 (Normal)Plenty of memory available
2 (Warn)Memory becoming constrained
4 (Critical)Memory severely limited
# Just get the pressure level
$ memory_pressure | grep "pressure level" | head -1
System memory pressure level: 1

# Monitor for pressure changes
$ while true; do
    level=$(memory_pressure 2>/dev/null | grep "System memory pressure" | awk '{print $NF}')
    echo "$(date): Pressure level $level"
    sleep 10
done

iostat - I/O Statistics

iostat displays disk and CPU I/O statistics.

Basic Usage

# Default output
$ iostat
              disk0
    KB/t  tps  MB/s
   24.00   45  1.05

# Update every 2 seconds, 5 times
$ iostat 2 5

# Show CPU statistics too
$ iostat -C

# Extended statistics
$ iostat -d -K -w 2

Output Interpretation

$ iostat -C -w 2
          cpu     load average        disk0
    us sy id 1m    5m   15m   KB/t  tps  MB/s
    12  5 83 1.25  2.30 2.15  24.0   45  1.05
ColumnMeaning
usUser CPU %
sySystem CPU %
idIdle CPU %
KB/tKilobytes per transfer
tpsTransfers per second
MB/sMegabytes per second

Per-Disk Statistics

# List all disks
$ iostat -d
              disk0               disk1
    KB/t  tps  MB/s      KB/t  tps  MB/s
   24.00   45  1.05     32.00   12  0.38

# Specific disk
$ iostat disk0

fs_usage - File System Usage

fs_usage traces file system activity in real-time.

Requirements

  • Requires root privileges
  • Terminal needs Full Disk Access for complete information

Basic Usage

# All filesystem activity
$ sudo fs_usage

# Filter by process name
$ sudo fs_usage -w Safari

# Filter by process ID
$ sudo fs_usage -p 12345

# Only show file activity (not network)
$ sudo fs_usage -f filesys

# Only show network activity
$ sudo fs_usage -f network

# Only show disk I/O
$ sudo fs_usage -f diskio

Output Interpretation

14:30:45.123  stat64        /usr/lib/libc.dylib      0.000023  Safari
14:30:45.124  open          /Users/david/file.txt    0.000045  Safari
14:30:45.125  read          F=5                      0.000012  Safari
14:30:45.126  close         F=5                      0.000003  Safari
ColumnMeaning
TimestampWhen the call occurred
System callType of operation
Path/DetailsFile path or file descriptor
DurationTime for the operation
ProcessProcess name

Filtering Examples

# Watch a specific directory
$ sudo fs_usage -w -f filesys | grep "/Users/david/project"

# Find what's writing to disk
$ sudo fs_usage -f diskio -w | grep "WrData"

# Watch for file deletions
$ sudo fs_usage -f filesys | grep -E "unlink|rmdir"

# Monitor Time Machine
$ sudo fs_usage -w backupd

Performance Analysis Script

#!/bin/bash
# io-summary.sh - Summarize I/O activity for a process

if [[ -z "$1" ]]; then
    echo "Usage: $0 <process-name> <duration-seconds>"
    exit 1
fi

PROCESS=$1
DURATION=${2:-10}

echo "Monitoring $PROCESS for $DURATION seconds..."
sudo timeout $DURATION fs_usage -w -f filesys "$PROCESS" 2>/dev/null | \
    awk '{print $2}' | sort | uniq -c | sort -rn | head -20

powermetrics - Power and Performance Metrics

powermetrics provides detailed power consumption and performance data, especially useful on laptops.

Requirements

  • Requires root privileges
  • More detailed on Apple Silicon

Basic Usage

# All metrics
$ sudo powermetrics

# Specific samplers
$ sudo powermetrics --samplers cpu_power,gpu_power,battery

# Sample once
$ sudo powermetrics -n 1

# Sample every 5 seconds
$ sudo powermetrics -i 5000

Available Samplers

# List available samplers
$ sudo powermetrics --samplers help

# Common samplers:
# cpu_power      - CPU power consumption
# gpu_power      - GPU power consumption
# battery        - Battery stats
# thermal        - Thermal state
# tasks          - Per-process power
# network        - Network power
# disk           - Disk power

CPU Power Output

$ sudo powermetrics --samplers cpu_power -n 1
Machine model: MacBookPro18,3
OS version: 14.0

*** Processor Info ***
CPU: Apple M1 Pro
CPU Complex Energy: 145 mJ

CPU Power: 2850 mW
E-Cluster Power: 450 mW
P-Cluster Power: 2400 mW

E-Cluster HW active frequency: 1200 MHz
E-Cluster HW active residency:  25.00%

P-Cluster HW active frequency: 3200 MHz
P-Cluster HW active residency:  45.00%

Battery Information

$ sudo powermetrics --samplers battery -n 1
*** Battery Info ***
Current Capacity: 85%
Design Capacity: 5103 mAh
Cycle Count: 234
Temperature: 32.5 C
Voltage: 12.45 V
Amperage: -1234 mA
Instant power: -15.37 W

Per-Process Power

$ sudo powermetrics --samplers tasks -n 1

*** Running tasks ***
Name                    PID     CPU_Time(ns)  CPU_Pct  Idle_Wake
Safari                  12345   234567890     12.5     234
Chrome                  23456   123456789     8.3      567
WindowServer            234     98765432      5.2      123

Thermal Information

$ sudo powermetrics --samplers thermal -n 1
*** Thermal State ***
System Thermal Level: 0 (nominal)
CPU Thermal Level: 0 (nominal)
GPU Thermal Level: 0 (nominal)

Monitoring Script

#!/bin/bash
# power-monitor.sh - Track power usage over time

echo "Timestamp,CPU_Power(W),GPU_Power(W),Battery(%),Amperage(mA)"
while true; do
    output=$(sudo powermetrics --samplers cpu_power,gpu_power,battery -n 1 2>/dev/null)

    cpu_power=$(echo "$output" | grep "CPU Power:" | awk '{print $3}')
    gpu_power=$(echo "$output" | grep "GPU Power:" | awk '{print $3}')
    battery=$(echo "$output" | grep "Current Capacity:" | awk '{print $3}' | tr -d '%')
    amperage=$(echo "$output" | grep "Amperage:" | awk '{print $2}')

    echo "$(date +%H:%M:%S),$cpu_power,$gpu_power,$battery,$amperage"
    sleep 30
done

sample - Process Sampling

sample captures a time-profile of a process, showing where it spends CPU time.

Basic Usage

# Sample for 5 seconds
$ sample Safari 5

# Save to file
$ sample Safari 5 -file /tmp/safari-sample.txt

# Sample by PID
$ sample 12345 5

Output Interpretation

Sampling process 12345 for 5 seconds with 1 millisecond of run time between samples
Sampling completed, processing symbols...
Analysis of sampling Safari (pid 12345) every 1 millisecond
Process: Safari [12345]
Path:    /Applications/Safari.app/Contents/MacOS/Safari

Call graph:
    2500 Thread_12345678   DispatchQueue_1: com.apple.main-thread  (serial)
      2500 start  (in dyld) + 1234  [0x12345678]
        2500 main  (in Safari) + 567  [0x23456789]
          1500 -[BrowserController loadURL:]  (in Safari) + 234
            1200 WebCore::FrameLoader::load()  (in WebCore) + 456
          1000 -[NSApplication run]  (in AppKit) + 789

Reading the call graph:

  • Numbers indicate sample counts (more = more time spent)
  • Indentation shows call hierarchy
  • Most time-consuming functions appear with highest counts

Finding Performance Issues

# Sample a slow process
$ sample SlowApp 10 -file /tmp/slowapp.txt

# Find heavy functions
$ grep -E "^[[:space:]]*[0-9]{3,}" /tmp/slowapp.txt | head -20

# Look for lock contention
$ grep -i "pthread_mutex\|semaphore\|lock" /tmp/slowapp.txt

spindump - System-Wide Sampling

spindump captures system-wide process sampling, especially useful for hangs.

# Basic spindump
$ sudo spindump

# Specific process
$ sudo spindump -pid 12345

# Duration and output
$ sudo spindump 5 1 -file /tmp/spindump.txt

Additional Useful Tools

iotop - I/O by Process

Not installed by default, but available via Homebrew:

$ brew install iotop

# Monitor I/O (requires root)
$ sudo iotop

nettop - Network by Process

Built into macOS:

# Interactive mode
$ nettop

# Process view
$ nettop -P

# One sample, machine-readable
$ nettop -P -L 1

sysctl - System Parameters

Query system information:

# CPU info
$ sysctl -n hw.ncpu
$ sysctl -n machdep.cpu.brand_string

# Memory info
$ sysctl -n hw.memsize

# All hardware info
$ sysctl hw

# Kernel stats
$ sysctl kern | head -20

Tool Comparison Summary

ToolPurposeRoot RequiredContinuous
topProcess monitorNoYes
htopEnhanced process monitorOptionalYes
vm_statMemory statisticsNoYes
memory_pressureMemory pressureNoNo
iostatI/O statisticsNoYes
fs_usageFile system traceYesYes
powermetricsPower analysisYesYes
sampleProcess profilingNoOne-shot
spindumpSystem samplingYesOne-shot
nettopNetwork by processNoYes

Quick Reference Commands

# CPU hogs
$ top -l 1 -o cpu -n 5 | tail -6

# Memory hogs
$ top -l 1 -o mem -n 5 | tail -6

# Memory pressure
$ memory_pressure | head -3

# Disk I/O rate
$ iostat -d -w 1 -c 3

# What's writing to disk
$ sudo fs_usage -f diskio -w | head -50

# Power consumption
$ sudo powermetrics --samplers cpu_power -n 1 | grep "CPU Power"

# Sample slow process
$ sample ProcessName 10 -file /tmp/sample.txt

# System-wide snapshot
$ sudo spindump 5 1 -file /tmp/spindump.txt

These command-line tools form the foundation of performance analysis on macOS, enabling detailed investigation of system behavior beyond what GUI tools provide.

Intel vs Apple Silicon Considerations

The transition from Intel to Apple Silicon represents the most significant architectural change in Mac history. Understanding the performance characteristics, compatibility considerations, and optimization strategies for each architecture is essential for both users and developers.

Architecture Overview

Intel Macs

Traditional x86_64 architecture:

┌─────────────────────────────────────────────────────────────────┐
│                    Intel Mac Architecture                        │
├─────────────────────────────────────────────────────────────────┤
│  CPU                           │  GPU                            │
│  ├── 4-8 Homogeneous Cores    │  ├── Intel Integrated          │
│  ├── Hyper-Threading          │  │   (shares system RAM)        │
│  ├── Up to ~5GHz boost        │  └── AMD Discrete (if present)  │
│  └── Turbo Boost              │      (dedicated VRAM)           │
├─────────────────────────────────────────────────────────────────┤
│  Memory                        │  Storage                        │
│  ├── DDR4 RAM                  │  ├── NVMe SSD                   │
│  ├── Separate from GPU         │  └── T2 encryption (some)      │
│  └── Upgradeable (some)        │                                 │
└─────────────────────────────────────────────────────────────────┘

Apple Silicon Macs

ARM-based System on Chip (SoC):

┌─────────────────────────────────────────────────────────────────┐
│               Apple Silicon Architecture (M-series)             │
├─────────────────────────────────────────────────────────────────┤
│  CPU Clusters                  │  GPU                            │
│  ├── Performance (P) Cores    │  ├── Integrated on SoC         │
│  │   High power, max speed    │  ├── 8-40 cores                 │
│  ├── Efficiency (E) Cores     │  └── Shares Unified Memory      │
│  │   Low power, background    │                                 │
│  └── QoS-based scheduling     │  Neural Engine                  │
├───────────────────────────────│  ├── 16 cores                   │
│  Unified Memory               │  └── ML acceleration            │
│  ├── Shared CPU/GPU/NE        │                                 │
│  ├── LPDDR5 on-package        │  Media Engine                   │
│  └── High bandwidth           │  └── Hardware encode/decode     │
├─────────────────────────────────────────────────────────────────┤
│  Secure Enclave │ Storage Controller │ Thunderbolt Controller   │
└─────────────────────────────────────────────────────────────────┘

Checking Architecture

System Information

# CPU architecture
$ uname -m
arm64  # Apple Silicon
x86_64 # Intel

# Detailed CPU info
$ sysctl -n machdep.cpu.brand_string
Apple M1 Pro

# Or for Intel:
# Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz

# System architecture
$ arch
arm64

# Check if running under Rosetta
$ sysctl sysctl.proc_translated 2>/dev/null
sysctl.proc_translated: 0  # Native
sysctl.proc_translated: 1  # Rosetta 2

Process Architecture

# Check a process's architecture
$ file /Applications/Safari.app/Contents/MacOS/Safari
/Applications/Safari.app/Contents/MacOS/Safari: Mach-O universal binary with 2 architectures
/Applications/Safari.app/Contents/MacOS/Safari (for architecture x86_64):    Mach-O 64-bit executable x86_64
/Applications/Safari.app/Contents/MacOS/Safari (for architecture arm64):     Mach-O 64-bit executable arm64

# List running processes with architecture
$ ps -eo pid,comm,arch | head -20

# In Activity Monitor, enable the "Architecture" column
# View > Columns > Architecture

Script to Check All Running Processes

#!/bin/bash
# check-architectures.sh - List running process architectures

echo "Native (arm64):"
echo "---------------"
ps -eo pid,comm | while read pid comm; do
    [[ "$pid" == "PID" ]] && continue
    arch=$(ps -p $pid -o arch= 2>/dev/null)
    [[ "$arch" == "arm64" ]] && echo "  $comm ($pid)"
done | head -20

echo ""
echo "Rosetta 2 (x86_64):"
echo "------------------"
ps -eo pid,comm | while read pid comm; do
    [[ "$pid" == "PID" ]] && continue
    arch=$(ps -p $pid -o arch= 2>/dev/null)
    [[ "$arch" == "x86_64" ]] && echo "  $comm ($pid)"
done

Apple Silicon Core Types

Understanding P-cores and E-cores

# Count cores by type
$ sysctl hw.perflevel0.physicalcpu  # Performance cores
hw.perflevel0.physicalcpu: 8

$ sysctl hw.perflevel1.physicalcpu  # Efficiency cores
hw.perflevel1.physicalcpu: 2

# Get detailed core information
$ sysctl hw.perflevel0.name
hw.perflevel0.name: Performance
$ sysctl hw.perflevel1.name
hw.perflevel1.name: Efficiency

Core Scheduling

macOS schedules threads based on Quality of Service (QoS):

QoS ClassDescriptionCore Preference
User InteractiveUI animations, event handlingP-cores
User InitiatedUser-requested tasksP-cores
DefaultNormal priorityP-cores or E-cores
UtilityLong-running tasksE-cores preferred
BackgroundNon-visible workE-cores only

Monitoring Core Usage

# Using powermetrics
$ sudo powermetrics --samplers cpu_power -n 1 | grep -E "Cluster|residency"

E-Cluster HW active residency:  25.00%
P-Cluster HW active residency:  45.00%

# Per-core usage not directly available from command line
# Use Activity Monitor's CPU History window (Window > CPU History)

Rosetta 2

Rosetta 2 translates x86_64 code to run on Apple Silicon.

How Rosetta Works

┌─────────────────────────────────────────────────────────────────┐
│                     Rosetta 2 Translation                        │
├─────────────────────────────────────────────────────────────────┤
│  x86_64 Binary                                                   │
│       │                                                          │
│       ▼                                                          │
│  Ahead-of-Time (AOT) Translation                                │
│  (First launch - translates entire binary)                      │
│       │                                                          │
│       ▼                                                          │
│  Cached Translated Code                                          │
│  (~/.Rosetta/cache)                                              │
│       │                                                          │
│       ▼                                                          │
│  Just-in-Time (JIT) Translation                                 │
│  (Handles dynamic code generation)                              │
│       │                                                          │
│       ▼                                                          │
│  Native arm64 Execution                                          │
└─────────────────────────────────────────────────────────────────┘

Installing Rosetta 2

# Check if Rosetta is installed
$ /usr/bin/pgrep -q oahd && echo "Rosetta is installed" || echo "Rosetta is not installed"

# Install Rosetta 2 (prompted automatically when needed)
$ softwareupdate --install-rosetta

# Install without prompt
$ softwareupdate --install-rosetta --agree-to-license

Running Apps Under Rosetta

# Force an app to run under Rosetta
$ arch -x86_64 /path/to/app

# Run Terminal under Rosetta
$ arch -x86_64 /bin/zsh

# Check current arch in shell
$ arch
i386  # Rosetta
arm64 # Native

# Run Homebrew under Rosetta (x86 Homebrew)
$ arch -x86_64 /usr/local/bin/brew install package

App Configuration

To always run an app under Rosetta:

  1. Find the app in Finder
  2. Right-click > Get Info
  3. Check “Open using Rosetta”

Or via command line:

# This requires modifying the app bundle (not recommended)
# Better to use arch -x86_64 when needed

Rosetta Performance

Typical Rosetta 2 overhead:

Workload TypeNative PerformanceRosetta Performance
CPU-bound100%~70-90%
Memory-bound100%~80-95%
I/O-bound100%~95-100%
Graphics (Metal)100%~70-80%

Factors affecting Rosetta performance:

  • First launch includes translation overhead
  • Subsequent launches use cached translation
  • JIT-compiled code (JavaScript, etc.) runs well
  • x86 SIMD instructions may have overhead

Identifying Rosetta Processes

# Check if a running process is translated
$ sysctl sysctl.proc_translated
sysctl.proc_translated: 0  # Native process
sysctl.proc_translated: 1  # Rosetta translated

# For another process
$ sysctl -p PID sysctl.proc_translated

# Or check process list
$ ps -eo pid,comm -x | while read pid name; do
    translated=$(sysctl -p $pid sysctl.proc_translated 2>/dev/null | awk '{print $2}')
    [[ "$translated" == "1" ]] && echo "Rosetta: $name (PID $pid)"
done

Rosetta Limitations

Features not supported under Rosetta 2:

FeatureStatus
AVX/AVX2/AVX-512Not supported
Kernel extensionsNot supported
Virtualization (hypervisor)Not supported
Some Intel-specific instructionsMay fail or trap
# Check if app uses AVX
$ otool -tV /path/to/binary | grep -E "vmov|vpadd|vpand" && echo "Uses AVX"

Unified Memory Architecture

How Unified Memory Works

┌─────────────────────────────────────────────────────────────────┐
│              Traditional Architecture (Intel)                    │
├─────────────────────────────────────────────────────────────────┤
│  CPU ←──── System Bus ────→ System RAM (DDR4)                   │
│              │                                                   │
│              └──── PCIe ────→ GPU ←→ VRAM (GDDR6)               │
│                                                                  │
│  Data must be copied between system RAM and VRAM                │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│              Unified Memory (Apple Silicon)                      │
├─────────────────────────────────────────────────────────────────┤
│       ┌─────────────────────────────────────────┐               │
│       │         Unified Memory Pool             │               │
│       │           (LPDDR5)                      │               │
│       └─────────────────────────────────────────┘               │
│           ▲           ▲           ▲                             │
│           │           │           │                             │
│         CPU         GPU      Neural Engine                      │
│                                                                  │
│  All components access the same memory pool                     │
│  No copying needed between CPU and GPU                          │
└─────────────────────────────────────────────────────────────────┘

Memory Information

# Total unified memory
$ sysctl hw.memsize | awk '{print $2/1024/1024/1024 " GB"}'
16 GB

# Memory bandwidth (varies by chip)
# M1: ~68 GB/s
# M1 Pro: ~200 GB/s
# M1 Max: ~400 GB/s
# M1 Ultra: ~800 GB/s

# Check memory pressure
$ memory_pressure
System-wide memory free percentage: 45%

Implications

AspectImpact
GPU memoryLimited only by total RAM
Data transferZero-copy CPU/GPU sharing
Memory pressureAffects both CPU and GPU
UpgradeNot possible (soldered)

Performance Comparison

Benchmarking Across Architectures

#!/bin/bash
# simple-bench.sh - Simple cross-architecture benchmark

echo "Architecture: $(uname -m)"
echo "CPU: $(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo 'Apple Silicon')"
echo ""

# CPU benchmark (simple)
echo "CPU Benchmark (compression):"
time dd if=/dev/zero bs=1m count=500 2>/dev/null | gzip > /dev/null

echo ""

# Memory benchmark
echo "Memory Benchmark:"
sysbench --test=memory run 2>/dev/null || echo "Install sysbench: brew install sysbench"

Real-World Performance Factors

FactorIntel AdvantageApple Silicon Advantage
Single-thread perfSimilar or lowerGenerally higher
Multi-thread perfHigher core counts (desktop)Better power efficiency
Power efficiencyLowerSignificantly higher
Thermal throttlingMore commonLess common
Boot time30-60 seconds10-15 seconds
Wake from sleep1-3 secondsInstant
x86 softwareNative~70-90% via Rosetta

Development Considerations

Universal Binaries

Universal binaries contain code for both architectures:

# Check if binary is universal
$ file /Applications/Safari.app/Contents/MacOS/Safari
Mach-O universal binary with 2 architectures:
- x86_64
- arm64

# List architectures with lipo
$ lipo -info /usr/bin/zip
Architectures in the fat file: /usr/bin/zip are: x86_64 arm64

# Extract single architecture
$ lipo /path/to/universal -thin arm64 -output /path/to/arm64-only

Building Universal Binaries

# Compile for both architectures
$ clang -arch arm64 -arch x86_64 -o myapp myapp.c

# Or separate builds
$ clang -arch arm64 -o myapp-arm64 myapp.c
$ clang -arch x86_64 -o myapp-x86_64 myapp.c
$ lipo -create myapp-arm64 myapp-x86_64 -output myapp

Homebrew on Apple Silicon

Apple Silicon Homebrew uses a different prefix:

ArchitectureHomebrew PrefixShell Path
Intel/usr/localAdded automatically
Apple Silicon/opt/homebrewRequires PATH setup
# Check Homebrew architecture
$ which brew
/opt/homebrew/bin/brew  # Apple Silicon native
/usr/local/bin/brew     # Intel or Rosetta

# Run Intel Homebrew under Rosetta
$ arch -x86_64 /usr/local/bin/brew install package

# Both Homebrews can coexist
# Native Homebrew
$ /opt/homebrew/bin/brew list
# Intel Homebrew (Rosetta)
$ /usr/local/bin/brew list

Docker and Virtualization

# Docker on Apple Silicon
# Uses ARM64 images by default, can run x86 via QEMU

# Check Docker architecture
$ docker version --format '{{.Server.Arch}}'
arm64

# Run x86 container explicitly
$ docker run --platform linux/amd64 image

# Native ARM containers run at full speed
# x86 containers run under QEMU emulation (~50% native speed)

Virtual Machines

VM TypeIntel MacApple Silicon Mac
macOS guestIntel macOSARM macOS only
Windows guestx86/x64 WindowsARM Windows only
Linux guestx86/x64 LinuxARM Linux (x86 via emulation)
Virtualization.frameworkNoYes
# Check virtualization support
$ sysctl kern.hv_support
kern.hv_support: 1  # Hypervisor supported

# On Apple Silicon, uses Virtualization.framework
# Tools: Parallels, VMware Fusion, UTM

Optimization Strategies

For Apple Silicon

  1. Use native apps when possible (check Architecture in Activity Monitor)
  2. Enable automatic graphics switching for battery life
  3. Leverage QoS in your code for proper core scheduling
  4. Use Metal for graphics and compute workloads
  5. Profile on target hardware - performance varies by chip

For Intel Macs

  1. Monitor thermal throttling with powermetrics
  2. Manage discrete GPU usage for battery life
  3. Check for AVX support in performance-critical code
  4. Use Turbo Boost wisely (can cause throttling)

Cross-Platform Development

# Compile optimized for each architecture
# ARM64:
$ clang -O3 -mcpu=apple-m1 -o myapp-arm64 myapp.c

# x86_64 with AVX2:
$ clang -O3 -march=haswell -o myapp-x86_64 myapp.c

# Create universal binary
$ lipo -create myapp-arm64 myapp-x86_64 -output myapp

Diagnostic Commands Summary

# Architecture basics
$ uname -m                    # Current architecture
$ arch                        # Same, shorter
$ sysctl sysctl.proc_translated  # Running under Rosetta?

# Apple Silicon specifics
$ sysctl hw.perflevel0.physicalcpu  # P-core count
$ sysctl hw.perflevel1.physicalcpu  # E-core count

# Binary inspection
$ file /path/to/binary        # Architecture(s) in binary
$ lipo -info /path/to/binary  # More detail on universal binaries

# Process architecture
$ ps -eo pid,comm,arch        # All processes with arch

# Rosetta
$ pgrep -q oahd && echo "Rosetta installed"
$ softwareupdate --install-rosetta  # Install Rosetta

Understanding the architectural differences between Intel and Apple Silicon Macs enables better performance optimization, compatibility management, and informed hardware decisions.

Memory Management Deep Dive

macOS employs sophisticated memory management that goes beyond traditional Unix approaches. Understanding how macOS handles memory, compression, swap, and memory pressure is essential for diagnosing performance issues and optimizing applications.

Memory Architecture Overview

Memory Hierarchy

┌─────────────────────────────────────────────────────────────────┐
│                    Memory Hierarchy                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  L1 Cache (per core)     ~192 KB     ~1 ns access              │
│           │                                                      │
│           ▼                                                      │
│  L2 Cache (per core)     ~3-12 MB    ~3-4 ns access            │
│           │                                                      │
│           ▼                                                      │
│  System Memory (RAM)     8-128 GB    ~100 ns access            │
│           │                                                      │
│           ▼                                                      │
│  Compressed Memory       Variable    ~500 ns access            │
│           │                                                      │
│           ▼                                                      │
│  Swap (SSD)              Variable    ~50-100 µs access          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Memory Categories in macOS

$ vm_stat
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free:                               45231.
Pages active:                            892341.
Pages inactive:                          234521.
Pages speculative:                        12345.
Pages throttled:                              0.
Pages wired down:                        456789.
Pages purgeable:                          23456.
...
CategoryDescriptionCan Be Reclaimed
FreeImmediately availableN/A
ActiveRecently used by appsYes, if needed
InactiveNot recently usedYes, readily
SpeculativePreemptively cached filesYes, readily
WiredKernel, drivers, systemNo
PurgeableApp-marked disposableYes, readily
CompressedInactive, compressed in RAMPartially

Memory Compression

macOS compresses inactive memory pages rather than immediately swapping to disk.

How Compression Works

┌─────────────────────────────────────────────────────────────────┐
│              Memory Compression Process                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Memory Pressure Increases                                       │
│           │                                                      │
│           ▼                                                      │
│  Identify Inactive Pages                                         │
│           │                                                      │
│           ▼                                                      │
│  Compress Pages (WKdm algorithm)                                │
│  ┌─────────────────────────────────────┐                        │
│  │  Original: 16 KB page               │                        │
│  │  Compressed: ~4-6 KB (typical)      │                        │
│  │  Ratio: 2.5-4x compression          │                        │
│  └─────────────────────────────────────┘                        │
│           │                                                      │
│           ▼                                                      │
│  Store in Compressor (VM region)                                │
│           │                                                      │
│           ▼                                                      │
│  Only swap if compressor full                                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Viewing Compression Statistics

$ vm_stat | grep -E "compressor|Compressions|Decompressions"
Pages stored in compressor:              567890.
Pages occupied by compressor:            123456.
Decompressions:                          345678.
Compressions:                            567890.

# Calculate compression ratio
$ vm_stat | awk '
/Pages stored in compressor/ {stored=$6}
/Pages occupied by compressor/ {occupied=$6}
END {
    if (occupied > 0) {
        ratio = stored / occupied
        printf "Compression ratio: %.2f:1\n", ratio
    }
}'

Compressor Memory Script

#!/bin/bash
# compressor-stats.sh - Show memory compressor statistics

PAGE_SIZE=$(pagesize)

stats=$(vm_stat)

stored=$(echo "$stats" | awk '/stored in compressor/ {print $6}' | tr -d '.')
occupied=$(echo "$stats" | awk '/occupied by compressor/ {print $6}' | tr -d '.')
compressions=$(echo "$stats" | awk '/^Compressions:/ {print $2}' | tr -d '.')
decompressions=$(echo "$stats" | awk '/^Decompressions:/ {print $2}' | tr -d '.')

stored_mb=$((stored * PAGE_SIZE / 1024 / 1024))
occupied_mb=$((occupied * PAGE_SIZE / 1024 / 1024))

if [[ $occupied -gt 0 ]]; then
    ratio=$(echo "scale=2; $stored / $occupied" | bc)
else
    ratio="N/A"
fi

echo "Memory Compressor Statistics"
echo "============================"
echo "Logical compressed:  ${stored_mb} MB (${stored} pages)"
echo "Physical compressor: ${occupied_mb} MB (${occupied} pages)"
echo "Compression ratio:   ${ratio}:1"
echo "Total compressions:  ${compressions}"
echo "Total decompressions: ${decompressions}"
echo ""
echo "Memory saved: $((stored_mb - occupied_mb)) MB"

Memory Pressure

macOS uses a memory pressure system to signal when memory is constrained.

Checking Memory Pressure

# Quick check
$ memory_pressure
System-wide memory free percentage: 42%
System memory pressure level: 1

The system memory pressure level is
currently: 1 (Normal)

Pressure Levels

LevelStateSystem Behavior
1NormalNo special action
2WarningCompress memory, notify apps
4CriticalAggressive compression, swap, terminate

Simulating Memory Pressure

For testing how apps respond to memory pressure:

# Simulate memory warning (apps receive notification)
$ memory_pressure -S -l warn

# Simulate critical memory
$ memory_pressure -S -l critical

# Release simulated pressure
$ memory_pressure -S -l normal

Monitoring Pressure Over Time

#!/bin/bash
# pressure-monitor.sh - Log memory pressure changes

echo "Monitoring memory pressure (Ctrl+C to stop)..."
last_level=0

while true; do
    level=$(memory_pressure 2>/dev/null | grep "pressure level:" | head -1 | awk '{print $NF}')

    if [[ "$level" != "$last_level" ]]; then
        case $level in
            1) state="NORMAL" ;;
            2) state="WARNING" ;;
            4) state="CRITICAL" ;;
            *) state="UNKNOWN" ;;
        esac
        echo "$(date): Memory pressure changed to $state (level $level)"
        last_level=$level
    fi

    sleep 5
done

Swap Management

macOS uses encrypted swap files when physical RAM and compressed memory are exhausted.

Swap Configuration

# Check swap usage
$ sysctl vm.swapusage
vm.swapusage: total = 2048.00M  used = 256.00M  free = 1792.00M  (encrypted)

# Swap file location
$ ls -la /private/var/vm/
total 524288
drwxr-xr-x  4 root  wheel       128 Jan 15 10:00 .
drwxr-xr-x  31 root  wheel       992 Jan 10 08:00 ..
-rw-------   1 root  wheel  2147483648 Jan 15 10:00 swapfile0
-rw-r--r--   1 root  wheel         0 Jan 15 10:00 swapfile.lock

# Swap statistics in vm_stat
$ vm_stat | grep -E "Swapins|Swapouts|Pageins|Pageouts"
Pageins:                                 123456.
Pageouts:                                 12345.
Swapins:                                    567.
Swapouts:                                   890.

Swap Activity Monitoring

# Watch for swap activity
$ vm_stat 1 | awk '
NR==1 {next}
NR==2 {header=1; next}
{
    if (header) {
        header=0
        print "Time       SwapIn  SwapOut  PageIn  PageOut"
    }
    # vm_stat continuous output format varies
    print strftime("%H:%M:%S"), $10, $11, $8, $9
}
'

Understanding Swap Impact

When swap is active:

Performance impact of memory location:
┌────────────────────────────────────────────────────────────────┐
│ Memory Type      │ Access Time    │ Relative Speed            │
├──────────────────┼────────────────┼───────────────────────────┤
│ RAM              │ ~100 ns        │ 1x (baseline)             │
│ Compressed RAM   │ ~500 ns        │ 5x slower                 │
│ SSD Swap         │ ~50-100 µs     │ 500-1000x slower          │
│ HDD Swap (old)   │ ~10 ms         │ 100,000x slower           │
└────────────────────────────────────────────────────────────────┘

Per-Process Memory Analysis

Using ps

# Memory columns: VSZ (virtual), RSS (resident)
$ ps aux | head -1
USER  PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND

# Top memory consumers
$ ps aux --sort=-%mem | head -10

# Specific process memory
$ ps -o pid,rss,vsz,comm -p 12345

Using top

# Memory-sorted view
$ top -o mem

# Show specific columns
$ top -stats pid,command,mem,rprvt,purg,cmprs,vprvt

Using vmmap

vmmap provides detailed memory mapping for a process:

# Full memory map
$ vmmap 12345

# Summary only
$ vmmap --summary 12345

# Example output:
$ vmmap --summary $(pgrep Safari)
Process:         Safari [12345]
Path:            /Applications/Safari.app/Contents/MacOS/Safari
...
ReadOnly portion of Libraries: Total=456.7M resident=234.5M(51%) swapped_out_or_unallocated=222.2M(49%)
Writable regions: Total=1.2G written=567.8M(47%) resident=890.1M(74%) swapped_out=0K(0%) unallocated=333.3M(28%)

VIRTUAL   RESIDENT    DIRTY  SWAPPED VOLATILE   NONVOL    EMPTY   REGION
SIZE      SIZE        SIZE   OUT SIZE            PURGEABLE PURGEABLE  DETAIL
========  ========  ========  ========  ========  ========  ======== ========
1.2G      890.1M    567.8M     0K       45.6M     12.3M     0K       TOTAL

Understanding vmmap Output

CategoryDescription
VIRTUAL SIZEAddress space allocated
RESIDENT SIZEActually in RAM
DIRTY SIZEModified, must be saved
SWAPPED OUTPaged to disk
PURGEABLECan be discarded

Memory Regions Script

#!/bin/bash
# process-memory.sh - Analyze process memory usage

if [[ -z "$1" ]]; then
    echo "Usage: $0 <process-name-or-pid>"
    exit 1
fi

if [[ "$1" =~ ^[0-9]+$ ]]; then
    PID=$1
else
    PID=$(pgrep -x "$1" | head -1)
    if [[ -z "$PID" ]]; then
        echo "Process not found: $1"
        exit 1
    fi
fi

PROCESS=$(ps -p $PID -o comm=)

echo "Memory Analysis for $PROCESS (PID: $PID)"
echo "=========================================="
echo ""

# Basic stats from ps
echo "From ps:"
ps -o rss=,vsz= -p $PID | awk '{
    printf "  Resident (RSS): %.1f MB\n", $1/1024
    printf "  Virtual (VSZ):  %.1f MB\n", $2/1024
}'

echo ""
echo "From vmmap summary:"
vmmap --summary $PID 2>/dev/null | grep -E "^(TOTAL|.*MALLOC|.*Writable)" | head -10

Diagnosing Memory Issues

High Memory Usage

# Find memory hogs
$ ps aux --sort=-%mem | head -10

# Detailed view of largest process
$ PID=$(ps aux --sort=-%mem | awk 'NR==2 {print $2}')
$ vmmap --summary $PID

Memory Leaks

# Check for growing memory over time
$ while true; do
    ps -o rss= -p $PID
    sleep 10
done

# Use leaks tool (requires debug symbols for best results)
$ leaks $PID

# Example output:
Process 12345: 234 nodes malloced for 567 KB
Process 12345: 5 leaks for 128 bytes

Memory Pressure Issues

#!/bin/bash
# diagnose-memory.sh - Memory diagnostics

echo "=== System Memory Overview ==="
vm_stat | head -20

echo ""
echo "=== Memory Pressure ==="
memory_pressure 2>/dev/null | head -5

echo ""
echo "=== Swap Usage ==="
sysctl vm.swapusage

echo ""
echo "=== Top Memory Consumers ==="
ps aux --sort=-%mem | head -6

echo ""
echo "=== Compressed Memory ==="
vm_stat | grep -E "compressor|Compression"

echo ""
echo "=== Recommendations ==="
FREE_PCT=$(memory_pressure 2>/dev/null | grep "free percentage" | awk '{print $5}' | tr -d '%')
if [[ -n "$FREE_PCT" ]] && [[ $FREE_PCT -lt 20 ]]; then
    echo "! Memory free is low ($FREE_PCT%). Consider closing applications."
fi

SWAPUSED=$(sysctl vm.swapusage | awk '{print $7}' | tr -d 'M')
if [[ "${SWAPUSED%.*}" -gt 0 ]]; then
    echo "! Swap is being used (${SWAPUSED}M). System may be slow."
fi

Memory Optimization

Purging Memory

The purge command forces disk cache to be purged:

# Clear disk caches (requires sudo)
$ sudo purge

# Note: This doesn't free app memory, only file caches
# Use for benchmarking with cold caches

Application-Level Optimization

Apps can respond to memory warnings:

# Check if app responds to memory warnings
$ log show --predicate 'eventMessage contains "memory"' --last 1h | grep -i warning

# Memory warning notifications
# Apps receive: NSProcessInfoPowerStateDidChange
# Or: UIApplicationDidReceiveMemoryWarningNotification (iOS ported apps)

Managing Wired Memory

Wired memory cannot be reclaimed. High wired memory indicates:

  • Many kernel extensions
  • Large file buffers
  • GPU memory allocations
# Check wired memory
$ vm_stat | grep "wired"
Pages wired down:                        456789.

# Convert to MB
$ vm_stat | awk -v ps=$(pagesize) '/wired/ {printf "Wired: %.0f MB\n", $4 * ps / 1024 / 1024}'

# List kernel extensions (can contribute to wired)
$ kextstat | wc -l

Unified Memory on Apple Silicon

Apple Silicon uses unified memory shared between CPU and GPU.

GPU Memory Allocation

# Check GPU memory usage
$ sudo powermetrics --samplers gpu_power -n 1 | grep -i memory

# In Activity Monitor:
# Enable GPU Memory column (View > Columns)

Implications

AspectTraditionalUnified Memory
GPU allocationDedicated VRAMShared RAM pool
Data transferCopy between RAM/VRAMZero-copy
Total availableRAM + VRAMSingle RAM pool
Memory pressureSeparateCombined

Advanced Memory Diagnostics

Using Instruments

For deep memory analysis, use Instruments (part of Xcode):

# Record memory allocations
$ xcrun xctrace record --template 'Allocations' --attach $PID --time-limit 30s

# Analyze leaks
$ xcrun xctrace record --template 'Leaks' --attach $PID --time-limit 30s

Memory Footprint Tool

# footprint command (requires Xcode command line tools)
$ footprint $PID

# Shows detailed memory attribution:
# - Dirty memory
# - Swapped memory
# - Compressed memory
# - IOKit mappings

System Memory Snapshot

#!/bin/bash
# memory-snapshot.sh - Comprehensive memory snapshot

echo "Memory Snapshot - $(date)"
echo "========================="
echo ""

# Hardware
echo "=== Hardware ==="
echo "Total RAM: $(sysctl -n hw.memsize | awk '{print $1/1024/1024/1024 " GB"}')"
echo "Page size: $(pagesize) bytes"
echo ""

# vm_stat summary
echo "=== VM Statistics ==="
vm_stat | head -15
echo ""

# Memory pressure
echo "=== Pressure ==="
memory_pressure 2>/dev/null | head -5
echo ""

# Swap
echo "=== Swap ==="
sysctl vm.swapusage
echo ""

# Top processes
echo "=== Top 10 by Memory ==="
printf "%-8s %-6s %12s %12s %s\n" "PID" "%MEM" "RSS(MB)" "VSZ(MB)" "COMMAND"
ps aux --sort=-%mem | awk 'NR>1 && NR<=11 {
    printf "%-8s %-6s %12.1f %12.1f %s\n", $2, $4, $6/1024, $5/1024, $11
}'

Summary

Key memory management concepts:

ConceptDescriptionCommand
Memory PressureSystem-wide memory demandmemory_pressure
CompressionIn-memory page compressionvm_stat | grep compressor
Wired MemoryNon-pageable kernel memoryvm_stat | grep wired
SwapDisk-backed virtual memorysysctl vm.swapusage
RSSResident Set Size (actual RAM)ps -o rss
VSZVirtual Size (address space)ps -o vsz

Critical commands for memory diagnosis:

# Quick health check
$ memory_pressure

# Detailed statistics
$ vm_stat

# Per-process analysis
$ vmmap --summary $PID

# Memory consumers
$ ps aux --sort=-%mem | head -10

# Swap status
$ sysctl vm.swapusage

# Check for leaks
$ leaks $PID

Understanding memory management helps diagnose slowdowns, optimize applications, and make informed decisions about system resources.

Disk I/O Optimization

Storage performance significantly impacts overall system responsiveness. This chapter covers measuring disk performance, understanding APFS optimizations, SSD health monitoring, and identifying I/O bottlenecks on macOS.

Storage Architecture on macOS

Modern Mac Storage Stack

┌─────────────────────────────────────────────────────────────────┐
│                     Applications                                 │
├─────────────────────────────────────────────────────────────────┤
│                     VFS (Virtual File System)                   │
├─────────────────────────────────────────────────────────────────┤
│                     APFS (File System)                          │
│   ├── Snapshots    ├── Clones    ├── Space Sharing             │
│   └── Encryption   └── Compression └── Sparse Files            │
├─────────────────────────────────────────────────────────────────┤
│                     Core Storage (optional legacy)              │
├─────────────────────────────────────────────────────────────────┤
│                     IOKit Storage Stack                         │
│   ├── Block Device Driver                                       │
│   └── NVMe Controller                                           │
├─────────────────────────────────────────────────────────────────┤
│                     Hardware                                     │
│   └── NVMe SSD (Apple Silicon) or SATA/NVMe SSD (Intel)        │
└─────────────────────────────────────────────────────────────────┘

Disk Information

# List all disks
$ diskutil list
/dev/disk0 (internal):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *500.1 GB   disk0
   1:             Apple_APFS_ISC                         524.3 MB   disk0s1
   2:                 Apple_APFS Container disk3         494.4 GB   disk0s2
   3:        Apple_APFS_Recovery                         5.4 GB     disk0s3

# APFS container details
$ diskutil apfs list
APFS Container Reference:     disk3
Size (Capacity Ceiling):      494384795648 B (494.4 GB)
Capacity In Use:              234567890123 B (234.6 GB)
Capacity Not Allocated:       259816905525 B (259.8 GB)

# Disk information
$ diskutil info disk0
   Device Identifier:         disk0
   Device Node:               /dev/disk0
   Whole:                     Yes
   Part of Whole:             disk0
   Device / Media Name:       APPLE SSD AP0512Q
   ...
   Solid State:               Yes
   Virtual:                   No
   ...

Measuring Disk Performance

Using iostat

# Basic disk statistics
$ iostat -d
              disk0
    KB/t  tps  MB/s
   24.00   45  1.05

# With CPU stats, every 2 seconds
$ iostat -C 2
          cpu     load average        disk0
    us sy id 1m    5m   15m   KB/t  tps  MB/s
    12  5 83 1.25  2.30 2.15  24.0   45  1.05

# Detailed disk stats
$ iostat -d -w 1 -c 5
              disk0
    KB/t  tps  MB/s
   32.00   89  2.78
   16.00  234  3.66
   48.00  156  7.31

Interpreting iostat:

MetricDescriptionHealthy Range
KB/tKilobytes per transferHigher = more efficient
tpsTransfers per secondDepends on workload
MB/sThroughputSSD: 500-3000+ MB/s

Using fs_usage

Real-time file system tracing:

# All file system activity
$ sudo fs_usage -f filesys

# Filter by process
$ sudo fs_usage -w Safari

# Only disk I/O (not cache hits)
$ sudo fs_usage -f diskio

# Specific file operations
$ sudo fs_usage -f filesys | grep -E "open|read|write|close"

Using iotop

# Install iotop
$ brew install iotop

# Monitor I/O by process
$ sudo iotop

# Example output:
# Total DISK READ:  15.2 MB/s | Total DISK WRITE:  5.4 MB/s
# PID   PRIO USER    DISK READ DISK WRITE  COMMAND
# 12345 BE   david   10.2 MB/s  2.3 MB/s   Safari
# 23456 BE   root     4.5 MB/s  3.1 MB/s   mds_stores

Benchmarking with dd

Simple sequential I/O test:

# Write test (sequential)
$ dd if=/dev/zero of=/tmp/testfile bs=1m count=1024 2>&1 | tail -1
1073741824 bytes transferred in 0.456789 secs (2350 MB/sec)

# Read test (clear cache first for accurate results)
$ sudo purge
$ dd if=/tmp/testfile of=/dev/null bs=1m 2>&1 | tail -1
1073741824 bytes transferred in 0.234567 secs (4577 MB/sec)

# Clean up
$ rm /tmp/testfile

More Accurate Benchmarking

# Install fio for comprehensive benchmarking
$ brew install fio

# Sequential read
$ fio --name=seqread --rw=read --bs=1m --size=1g --numjobs=1 --runtime=30 --filename=/tmp/fiotest

# Sequential write
$ fio --name=seqwrite --rw=write --bs=1m --size=1g --numjobs=1 --runtime=30 --filename=/tmp/fiotest

# Random read (4K blocks, queue depth 32)
$ fio --name=randread --rw=randread --bs=4k --size=1g --numjobs=1 --iodepth=32 --runtime=30 --filename=/tmp/fiotest

# Random write
$ fio --name=randwrite --rw=randwrite --bs=4k --size=1g --numjobs=1 --iodepth=32 --runtime=30 --filename=/tmp/fiotest

# Clean up
$ rm /tmp/fiotest

APFS Optimizations

Space Sharing

APFS volumes share space within a container:

# View space sharing
$ diskutil apfs list
+-- Container disk3 (494.4 GB capacity)
    +-- Volume disk3s1 (Macintosh HD - Data) - 200.5 GB used
    +-- Volume disk3s2 (Macintosh HD) - 15.2 GB used
    +-- Volume disk3s3 (Preboot) - 234 MB used
    +-- Volume disk3s5 (VM) - 2.1 GB used
    Unallocated: 276.4 GB

# All volumes can use the unallocated space

Snapshots

APFS snapshots are instant, space-efficient point-in-time copies:

# List snapshots
$ tmutil listlocalsnapsshots /
com.apple.TimeMachine.2024-01-15-120000.local

# Snapshot space usage
$ diskutil apfs listSnapshots disk3s1
Snapshots for disk3s1 (4 found):
+-- 12345678-1234-1234-1234-123456789012
|   Name:        com.apple.TimeMachine.2024-01-15-120000.local
|   XID:         12345
|   Created:     2024-01-15 12:00:00
|   Purgeable:   Yes

# Delete a snapshot (frees space)
$ sudo tmutil deletelocalsnapshots 2024-01-15-120000

# Snapshot disk usage
$ diskutil apfs listSnapshots disk3s1 | grep -A2 "Purgeable Space"

Clones

APFS clones share data blocks until modified:

# Clone a file (instant, shares blocks)
$ cp -c largefile.dmg largefile-clone.dmg

# Check if files share blocks (no direct command, but)
# Both files show full size but disk usage is shared

# Verify with disk usage
$ du -sh largefile.dmg largefile-clone.dmg
10G    largefile.dmg
10G    largefile-clone.dmg

$ df -h .  # Only uses ~10G, not 20G

Sparse Files

APFS handles sparse files efficiently:

# Create a sparse file
$ dd if=/dev/zero of=sparsefile bs=1 count=0 seek=10g

# Check apparent vs actual size
$ ls -lh sparsefile
-rw-r--r--  1 david  staff    10G Jan 15 12:00 sparsefile

$ du -sh sparsefile
  0B    sparsefile

SSD Considerations

TRIM Support

TRIM helps SSDs maintain performance by informing them of deleted blocks:

# Check TRIM status
$ system_profiler SPSerialATADataType | grep -i trim
TRIM Support: Yes

# For NVMe (Apple Silicon, newer Intel Macs)
$ system_profiler SPNVMeDataType | grep -i trim

macOS enables TRIM automatically for Apple SSDs. Third-party SSDs may require:

# Enable TRIM for third-party SSDs (Intel Macs, requires reboot)
$ sudo trimforce enable

SSD Health Monitoring

# Using smartctl (install smartmontools)
$ brew install smartmontools

# Check SSD health
$ sudo smartctl -a /dev/disk0

# Key metrics to watch:
# - Percentage Used (wear indicator)
# - Available Spare
# - Media and Data Integrity Errors

# Quick health check
$ sudo smartctl -H /dev/disk0
SMART overall-health self-assessment test result: PASSED

# View wear information
$ sudo smartctl -a /dev/disk0 | grep -E "Percentage Used|Available Spare|Data Units"

Wear Leveling

SSDs have limited write cycles. Monitor write volume:

# Total bytes written (smartctl)
$ sudo smartctl -a /dev/disk0 | grep "Data Units Written"
Data Units Written: 12,345,678 [6.32 TB]

# Daily write rate estimation
# If SSD is 6 months old with 6.32 TB written:
# 6.32 TB / 180 days = ~35 GB/day

Identifying I/O Bottlenecks

Signs of I/O Bottleneck

SymptomPossible Cause
Beach ball cursorApp waiting on I/O
Slow app launchDisk reading app files
System unresponsiveHeavy disk activity
High CPU waitI/O blocking CPU work

Diagnosing with fs_usage

#!/bin/bash
# io-bottleneck.sh - Find I/O bottlenecks

echo "Monitoring disk I/O for 30 seconds..."
echo "Top 20 files being accessed:"
echo ""

sudo timeout 30 fs_usage -f diskio 2>/dev/null | \
    awk '{print $NF}' | \
    grep -v "^$" | \
    sort | uniq -c | sort -rn | head -20

Finding Heavy I/O Processes

# Using iostat to see overall disk load
$ iostat -C 1

# Then use fs_usage to find which process
$ sudo fs_usage -f diskio 2>/dev/null | head -100

# Or use Activity Monitor > Disk tab
# Sort by "Bytes Written" or "Bytes Read"

Common I/O Hogs

ProcessActivitySolution
mds, mds_storesSpotlight indexingWait, or exclude folders
backupdTime MachineSchedule backups
photolibrarydPhotos libraryWait for completion
birdiCloud syncCheck iCloud status
nsurlsessiondBackground downloadsCheck for updates

Excluding Folders from Spotlight

# Add exclusion via mdutil
$ sudo mdutil -i off /Volumes/ExternalDrive

# Or for specific folders, use Spotlight preferences
# Or via command:
$ sudo defaults write /.Spotlight-V100/VolumeConfiguration Exclusions -array-add "/path/to/exclude"
$ sudo mdutil -E /

I/O Optimization Strategies

For General Use

# 1. Monitor current I/O
$ iostat -C 2

# 2. Identify heavy writers
$ sudo fs_usage -f diskio 2>/dev/null | awk '{print $NF}' | sort | uniq -c | sort -rn | head -10

# 3. Check for Spotlight indexing
$ sudo fs_usage mds_stores

# 4. Verify APFS is healthy
$ diskutil verifyVolume /

For Development

# Exclude build directories from Spotlight
$ touch ~/Projects/.metadata_never_index

# Use RAM disk for temp files (speeds up builds)
$ diskutil erasevolume HFS+ "RAMDisk" $(hdiutil attach -nomount ram://4194304)
# Creates 2GB RAM disk

# Place build output on RAM disk
$ ln -s /Volumes/RAMDisk/build ~/Projects/myproject/build

For Large File Operations

# Use cp -c for APFS clones (instant copy)
$ cp -c largefile.dmg copy.dmg

# For move operations, same volume is instant (no copy)
$ mv largefile.dmg /same/volume/newlocation/

# For cross-volume, use rsync with progress
$ rsync -ah --progress source/ destination/

Monitoring Script

#!/bin/bash
# disk-monitor.sh - Comprehensive disk monitoring

echo "=== Disk Health Check ==="
echo "Date: $(date)"
echo ""

echo "=== Storage Overview ==="
df -h / /System/Volumes/Data 2>/dev/null | awk 'NR==1 || /disk/'
echo ""

echo "=== APFS Container ==="
diskutil apfs list 2>/dev/null | grep -E "Container|Capacity|Volume" | head -20
echo ""

echo "=== Recent I/O Statistics ==="
iostat -d -c 3 -w 1
echo ""

echo "=== Top I/O Processes (5 second sample) ==="
sudo timeout 5 fs_usage -f diskio 2>/dev/null | \
    awk '{print $NF}' | grep -v "^$" | \
    sort | uniq -c | sort -rn | head -10

echo ""
echo "=== Snapshot Usage ==="
SNAPSHOTS=$(tmutil listlocalsnapshots / 2>/dev/null | wc -l)
echo "Local snapshots: $SNAPSHOTS"

echo ""
echo "=== Disk Write Statistics ==="
if command -v smartctl &>/dev/null; then
    sudo smartctl -a /dev/disk0 2>/dev/null | grep -E "Data Units Written|Percentage Used" || echo "SMART data not available"
else
    echo "Install smartmontools for SSD health: brew install smartmontools"
fi

APFS Performance Tuning

Defragmentation

APFS does not require or support traditional defragmentation. However:

# APFS automatically:
# - Optimizes file placement
# - Uses copy-on-write to reduce fragmentation
# - Maintains metadata efficiently

# If you suspect fragmentation issues:
# 1. Check available space (low space = more fragmentation)
$ df -h /

# 2. Backup and restore is the nuclear option
# (Recreates file layout optimally)

Volume Group Configuration

# View volume group (System + Data)
$ diskutil apfs listVolumeGroups

# Modern macOS uses:
# - Macintosh HD (system, read-only)
# - Macintosh HD - Data (user data)
# This improves security and update reliability

Summary

Key disk I/O concepts:

ToolPurposeUsage
iostatI/O statisticsiostat -d -w 2
fs_usageReal-time I/O tracingsudo fs_usage -f diskio
diskutilDisk managementdiskutil apfs list
smartctlSSD healthsudo smartctl -a /dev/disk0
tmutilSnapshot managementtmutil listlocalsnapshots /

APFS features for performance:

FeatureBenefit
Space sharingEfficient multi-volume usage
ClonesInstant file copies
SnapshotsFast point-in-time copies
Sparse filesEfficient large file handling
Copy-on-writeReduced writes, data integrity

Quick diagnostic commands:

# I/O overview
$ iostat -C 2

# Find I/O sources
$ sudo fs_usage -f diskio | head -50

# Check disk space
$ df -h /

# List snapshots
$ tmutil listlocalsnapshots /

# SSD health
$ sudo smartctl -H /dev/disk0

# APFS status
$ diskutil apfs list

Understanding disk I/O patterns and APFS features helps maintain optimal storage performance on macOS.

Power Management and Battery

Effective power management extends battery life on laptops and reduces energy consumption on desktops. macOS provides sophisticated power management through pmset, caffeinate, power assertions, and App Nap. Understanding these systems enables optimization for both performance and efficiency.

Power Management Architecture

Power Management Layers

┌─────────────────────────────────────────────────────────────────┐
│                    Applications                                  │
│          (Power assertions, App Nap responses)                  │
├─────────────────────────────────────────────────────────────────┤
│                    WindowServer                                  │
│          (Display sleep, screen brightness)                     │
├─────────────────────────────────────────────────────────────────┤
│                    IOKit Power Management                       │
│          (Device power states, sleep/wake)                      │
├─────────────────────────────────────────────────────────────────┤
│                    Kernel                                        │
│          (CPU power states, thermal management)                 │
├─────────────────────────────────────────────────────────────────┤
│                    SMC (System Management Controller)           │
│          (Battery, fans, sensors - Intel Macs)                  │
│                    or                                            │
│                    Apple Silicon Power Management               │
│          (Integrated power controller)                          │
└─────────────────────────────────────────────────────────────────┘

pmset - Power Management Settings

pmset is the primary tool for configuring power settings.

Viewing Current Settings

# All current settings
$ pmset -g
System-wide power settings:
Currently in use:
 standby              1
 Sleep On Power Button 1
 hibernatefile        /var/vm/sleepimage
 powernap             1
 networkoversleep     0
 disksleep            10
 sleep                1 (sleep prevented by screensharingd)
 hibernatemode        3
 ttyskeepawake        1
 displaysleep         10
 tcpkeepalive         1
 lowpowermode         0
 womp                 0

# Battery status
$ pmset -g batt
Now drawing from 'Battery Power'
 -InternalBattery-0 (id=12345678)    85%; discharging; 4:30 remaining present: true

# Power assertions (what's preventing sleep)
$ pmset -g assertions
2024-01-15 12:00:00 -0800
Assertion status system-wide:
   BackgroundTask                 1
   ApplePushServiceTask           0
   UserIsActive                   1
   PreventUserIdleDisplaySleep    1
   PreventSystemSleep             0
   ExternalMedia                  0
   PreventUserIdleSystemSleep     1
   NetworkClientActive            0
Listed by owning process:
   pid 12345(Safari): [0x0000123400000abc] 01:23:45 PreventUserIdleDisplaySleep named: "Playing Audio"

Power Source Settings

Different settings for battery vs AC power:

# View settings by power source
$ pmset -g custom
Battery Power:
 displaysleep         2
 disksleep            10
 sleep                10
 womp                 0
 powernap             0
AC Power:
 displaysleep         10
 disksleep            10
 sleep                0
 womp                 1
 powernap             1

Configuring Power Settings

# Set display sleep timeout (minutes)
$ sudo pmset -b displaysleep 5    # Battery: 5 minutes
$ sudo pmset -c displaysleep 15   # AC: 15 minutes
$ sudo pmset -a displaysleep 10   # All: 10 minutes

# Set system sleep timeout
$ sudo pmset -a sleep 30

# Disable sleep entirely
$ sudo pmset -a sleep 0
$ sudo pmset -a disablesleep 1

# Enable/disable wake on network (Wake on LAN)
$ sudo pmset -a womp 1  # Enable
$ sudo pmset -a womp 0  # Disable

# Power Nap settings
$ sudo pmset -a powernap 0  # Disable Power Nap
$ sudo pmset -a powernap 1  # Enable Power Nap

Important pmset Keys

KeyDescriptionValues
displaysleepDisplay sleep timeout (min)0 = never, N = minutes
disksleepDisk sleep timeout (min)0 = never, N = minutes
sleepSystem sleep timeout (min)0 = never, N = minutes
wompWake on Magic Packet (LAN)0/1
powernapPower Nap enabled0/1
hibernatemodeHibernate mode0, 3, or 25
standbyStandby mode0/1
standbydelayDelay before standby (sec)seconds
autopoweroffAuto power off0/1
lowpowermodeLow Power Mode0/1
tcpkeepaliveTCP keepalive during sleep0/1

Hibernate Modes

# Check current hibernate mode
$ pmset -g | grep hibernatemode

# Hibernate modes:
# 0 = RAM stays powered, no hibernation (desktop default)
# 3 = RAM powered + hibernation file (laptop default, safe sleep)
# 25 = Pure hibernation, RAM powered off (maximum battery preservation)

# Set hibernate mode
$ sudo pmset -a hibernatemode 3

Mode comparison:

ModeRAM PowerHibernate FileWake TimeBattery Use
0Always onNoInstantHigher
3On until standbyYesInstant/SlowMedium
25OffYesSlowMinimal

Low Power Mode

Available on MacBooks with Apple Silicon and recent Intel:

# Check Low Power Mode
$ pmset -g | grep lowpowermode

# Enable Low Power Mode
$ sudo pmset -a lowpowermode 1

# Disable
$ sudo pmset -a lowpowermode 0

Low Power Mode effects:

  • Reduces CPU performance
  • Dims display
  • Reduces background activity
  • Extends battery life significantly

caffeinate - Prevent Sleep

caffeinate prevents the system from sleeping.

Basic Usage

# Prevent sleep while command runs
$ caffeinate -s make all

# Prevent sleep for duration (seconds)
$ caffeinate -t 3600  # 1 hour

# Prevent display sleep
$ caffeinate -d

# Prevent idle sleep
$ caffeinate -i

# Prevent disk sleep
$ caffeinate -m

# Prevent system sleep
$ caffeinate -s

# All of the above
$ caffeinate -dims

caffeinate Options

OptionPreventsUse Case
-dDisplay sleepPresentations
-iIdle sleepLong-running tasks
-mDisk sleepLarge file operations
-sSystem sleepDownloads, backups
-uDeclare user activeSimulate user activity
-t NAll for N secondsTime-limited prevention
-w PIDWhile PID runsFollow a process

Practical Examples

# Keep awake during long compile
$ caffeinate -i make -j8 all

# Keep awake while download completes
$ caffeinate -s curl -O https://example.com/large-file.zip

# Keep awake while backup runs
$ caffeinate -s rsync -av /source /destination

# Keep display on during presentation (until Ctrl+C)
$ caffeinate -d

# Keep awake as long as another process runs
$ long_running_process &
$ caffeinate -w $!

# Script that manages its own caffeinate
#!/bin/bash
caffeinate -i -w $$ &
# ... long running work ...
# caffeinate automatically exits when script finishes

Scripted Power Control

#!/bin/bash
# smart-caffeinate.sh - Caffeinate with status

DURATION=${1:-3600}

echo "Preventing sleep for $((DURATION / 60)) minutes"
echo "Press Ctrl+C to cancel"

caffeinate -dims -t $DURATION &
CAFF_PID=$!

trap "kill $CAFF_PID 2>/dev/null; echo 'Sleep prevention cancelled'" EXIT

while kill -0 $CAFF_PID 2>/dev/null; do
    REMAINING=$((DURATION - SECONDS))
    if [[ $REMAINING -lt 0 ]]; then
        break
    fi
    printf "\rRemaining: %02d:%02d" $((REMAINING / 60)) $((REMAINING % 60))
    sleep 1
done

echo -e "\nSleep prevention ended"

Power Assertions

Power assertions are how applications communicate their power needs to the system.

Viewing Assertions

# List all active assertions
$ pmset -g assertions

# Detailed assertion info
$ pmset -g assertionslog

# Who's preventing sleep?
$ pmset -g assertions | grep -A2 "PreventSystemSleep\|PreventUserIdleSystemSleep"

Common Assertion Types

AssertionEffect
PreventUserIdleSystemSleepSystem won’t idle sleep
PreventUserIdleDisplaySleepDisplay won’t idle sleep
PreventSystemSleepSystem cannot sleep at all
NoIdleSleepAssertionLegacy, same as PreventUserIdleSystemSleep
NoDisplaySleepAssertionLegacy, same as PreventUserIdleDisplaySleep

Creating Assertions from Command Line

# caffeinate creates assertions internally

# To create specific assertions programmatically:
# Use IOKit APIs (requires code)
# Or use caffeinate with appropriate flags

Finding Assertion Offenders

#!/bin/bash
# find-assertion-owners.sh - Find processes preventing sleep

echo "Processes with active power assertions:"
echo "======================================="

pmset -g assertions | grep -E "^\s+pid" | while read line; do
    pid=$(echo "$line" | awk -F'[()]' '{print $2}')
    name=$(echo "$line" | awk -F'[()]' '{print $1}' | awk '{print $2}')
    assertion=$(echo "$line" | awk -F'"' '{print $2}')

    echo "Process: $name (PID $pid)"
    echo "  Assertion: $assertion"
    echo ""
done

App Nap

App Nap reduces resource usage for applications not actively being used.

How App Nap Works

┌─────────────────────────────────────────────────────────────────┐
│                    App Nap Conditions                            │
├─────────────────────────────────────────────────────────────────┤
│ App enters App Nap when:                                        │
│ 1. Window is not visible (minimized, behind other windows)     │
│ 2. Not playing audio                                            │
│ 3. No active power assertions                                   │
│ 4. Not explicitly disabled by app                               │
├─────────────────────────────────────────────────────────────────┤
│ App Nap effects:                                                │
│ - Timer coalescing (batched background work)                    │
│ - Reduced I/O priority                                          │
│ - Reduced CPU priority                                          │
│ - Paused or throttled network activity                          │
└─────────────────────────────────────────────────────────────────┘

Checking App Nap Status

In Activity Monitor:

  1. View > Columns > App Nap
  2. “Yes” = App Nap active, “No” = App running normally
# Check if an app supports App Nap
$ defaults read /Applications/Safari.app/Contents/Info.plist NSAppSleepDisabled 2>/dev/null
# (no output or error = App Nap enabled)
# 1 = App Nap disabled

Disabling App Nap for an App

# Disable App Nap for specific app
$ defaults write com.apple.Safari NSAppSleepDisabled -bool YES

# Via Finder:
# 1. Right-click app in Applications
# 2. Get Info
# 3. Check "Prevent App Nap"

Power Monitoring

Battery Information

# Battery status
$ pmset -g batt
Now drawing from 'Battery Power'
 -InternalBattery-0 (id=12345678)    85%; discharging; 4:30 remaining

# Detailed battery info
$ system_profiler SPPowerDataType

# Battery health (cycles, condition)
$ system_profiler SPPowerDataType | grep -E "Cycle Count|Condition|Maximum Capacity"
      Cycle Count: 234
      Condition: Normal
      Maximum Capacity: 92%

# ioreg for detailed battery data
$ ioreg -rn AppleSmartBattery | grep -E "Cycle|Capacity|Temperature"

Power Consumption Monitoring

# Using powermetrics (requires sudo)
$ sudo powermetrics --samplers cpu_power,battery -n 1

# CPU power consumption
$ sudo powermetrics --samplers cpu_power -n 1 | grep "CPU Power"
CPU Power: 2850 mW

# Battery drain rate
$ sudo powermetrics --samplers battery -n 1 | grep "Amperage"
Amperage: -1234 mA

# Continuous monitoring
$ sudo powermetrics --samplers cpu_power,battery -i 5000 | grep -E "CPU Power|Battery Level"

Energy Impact by Process

# Using top
$ top -o power

# Using powermetrics for detailed per-process
$ sudo powermetrics --samplers tasks -n 1 | head -40

Power Monitoring Script

#!/bin/bash
# power-monitor.sh - Monitor power consumption

echo "Power Monitor - Press Ctrl+C to stop"
echo "Time,Battery%,Amperage(mA),CPU_Power(mW),Drawing_From"

while true; do
    # Get battery info
    batt_info=$(pmset -g batt)
    battery_pct=$(echo "$batt_info" | grep -o '[0-9]*%' | head -1 | tr -d '%')
    power_source=$(echo "$batt_info" | grep "drawing from" | awk -F"'" '{print $2}')

    # Get power metrics (needs sudo)
    power_data=$(sudo powermetrics --samplers cpu_power,battery -n 1 2>/dev/null)
    cpu_power=$(echo "$power_data" | grep "CPU Power:" | awk '{print $3}' | tr -d 'mW')
    amperage=$(echo "$power_data" | grep "Amperage:" | awk '{print $2}' | tr -d 'mA')

    echo "$(date +%H:%M:%S),$battery_pct,$amperage,$cpu_power,$power_source"

    sleep 30
done

Optimizing Battery Life

System Settings

# Maximize battery life settings
$ sudo pmset -b displaysleep 2      # Display off after 2 min on battery
$ sudo pmset -b sleep 5              # System sleep after 5 min
$ sudo pmset -b powernap 0           # Disable Power Nap on battery
$ sudo pmset -b lowpowermode 1       # Enable Low Power Mode
$ sudo pmset -b lessbright 1         # Slightly dim display on battery

# Aggressive standby
$ sudo pmset -b standby 1
$ sudo pmset -b standbydelay 60      # Enter standby after 1 min of sleep
$ sudo pmset -b hibernatemode 25     # Pure hibernation (slowest wake, best battery)

Finding Battery Drains

#!/bin/bash
# find-battery-drains.sh - Identify battery draining activities

echo "=== Power Assertions ==="
pmset -g assertions | grep -E "pid|named"

echo ""
echo "=== High Energy Impact Processes ==="
echo "(Check Activity Monitor > Energy tab for more detail)"
ps aux | sort -nrk 3 | head -6

echo ""
echo "=== Apps Preventing Sleep ==="
pmset -g assertions | grep "PreventUserIdleSystemSleep\|PreventSystemSleep" -A2

echo ""
echo "=== Bluetooth Devices ==="
system_profiler SPBluetoothDataType 2>/dev/null | grep -E "Connected:|Name:" | head -10

echo ""
echo "=== Active Network Connections ==="
lsof -i | grep -E "ESTABLISHED|LISTEN" | awk '{print $1}' | sort | uniq -c | sort -rn | head -10

Quick Battery Tips

  1. Reduce screen brightness - Major power consumer
  2. Disable Bluetooth/Wi-Fi when not needed
  3. Close unused tabs - Browser tabs use memory and CPU
  4. Enable Low Power Mode - Significant battery extension
  5. Check Activity Monitor Energy tab - Find specific drains
  6. Disable Power Nap on battery - Prevents background wake
  7. Use native apps - Rosetta 2 uses more power

Thermal Management

macOS manages thermals automatically, but you can monitor:

# Thermal state
$ sudo powermetrics --samplers thermal -n 1
*** Thermal State ***
System Thermal Level: 0 (nominal)
CPU Thermal Level: 0 (nominal)
GPU Thermal Level: 0 (nominal)

# Fan speed (Intel Macs with SMC)
$ sudo powermetrics --samplers smc -n 1 | grep -i fan

# Or use third-party tools
$ brew install osx-cpu-temp
$ osx-cpu-temp  # Shows CPU temperature

Thermal States

LevelStateBehavior
0NominalNormal operation
1ModerateSlight throttling
2-9ElevatedIncreasing throttling
10+CriticalAggressive throttling, possible shutdown

Summary

Key power management commands:

CommandPurposeExample
pmset -gView settingspmset -g batt
pmset -aSet for all sourcessudo pmset -a sleep 30
pmset -bSet for batterysudo pmset -b lowpowermode 1
pmset -cSet for AC powersudo pmset -c sleep 0
caffeinatePrevent sleepcaffeinate -s make all
powermetricsPower analysissudo powermetrics --samplers cpu_power

Power optimization checklist:

# Check current state
$ pmset -g batt              # Battery status
$ pmset -g assertions        # What's preventing sleep
$ pmset -g                   # Current power settings

# Common optimizations
$ sudo pmset -b lowpowermode 1      # Enable Low Power Mode
$ sudo pmset -b displaysleep 2      # Quick display off
$ sudo pmset -b powernap 0          # Disable Power Nap on battery
$ caffeinate -i long_task           # Prevent sleep during task

# Monitor power use
$ sudo powermetrics --samplers cpu_power,battery -n 1

Effective power management balances performance needs with battery life and energy efficiency.

Troubleshooting Performance Issues

When macOS feels slow, a systematic diagnostic approach helps identify the root cause. This chapter provides workflows for diagnosing CPU, memory, disk, network, and graphics performance issues, along with common problems and their solutions.

Diagnostic Framework

The Performance Triangle

Performance issues typically fall into three categories:

                    CPU
                   /   \
                  /     \
                 /       \
                /  System \
               /  Resources \
              /             \
           Memory --------- I/O
              (RAM, Swap)    (Disk, Network)

When investigating performance:

  1. Check all three resource types
  2. Identify which is the bottleneck
  3. Investigate the specific resource
  4. Address the root cause

Quick Health Check

Start every investigation with this script:

#!/bin/bash
# quick-check.sh - Rapid system health assessment

echo "=== System Health Check - $(date) ==="
echo ""

# 1. Load Average
echo "--- Load Average ---"
uptime
NCPU=$(sysctl -n hw.ncpu)
echo "CPU count: $NCPU"
echo ""

# 2. CPU Usage
echo "--- CPU Usage ---"
top -l 1 -n 0 | grep "CPU usage"
echo ""

# 3. Memory Pressure
echo "--- Memory Pressure ---"
memory_pressure 2>/dev/null | head -3
echo ""

# 4. Disk Space
echo "--- Disk Space ---"
df -h / | tail -1
echo ""

# 5. Top Processes
echo "--- Top 5 by CPU ---"
ps aux | sort -nrk 3 | head -6
echo ""

echo "--- Top 5 by Memory ---"
ps aux | sort -nrk 4 | head -6

CPU Troubleshooting

Symptoms

  • Fans running constantly
  • System feels sluggish
  • Beach ball cursor
  • High CPU temperature
  • Short battery life

Diagnostic Steps

# Step 1: Check overall CPU usage
$ top -l 1 -n 0 | grep "CPU usage"
CPU usage: 85.0% user, 10.0% sys, 5.0% idle

# Step 2: Find CPU-heavy processes
$ top -l 1 -o cpu -n 10 | tail -11

# Step 3: Check if process is stuck (sample it)
$ sample PID 5 -file /tmp/sample.txt
$ head -50 /tmp/sample.txt

# Step 4: Check for runaway threads
$ ps -M -p PID  # Thread list for process

# Step 5: Verify process is responsive
$ kill -0 PID && echo "Process is alive" || echo "Process may be zombie"

Common CPU Issues

IssueCauseSolution
kernel_task high CPUThermal throttlingCool the Mac, check vents
mds_stores high CPUSpotlight indexingWait, or exclude folders
WindowServer high CPUGraphics issueReduce transparency, check GPU
nsurlsessiondBackground downloadsCheck for updates
softwareupdatedSystem updatesLet it complete
Single app 100%+ CPUApp issueRestart app, check for updates

Investigating kernel_task

kernel_task artificially uses CPU to throttle when the system is too hot:

# Check thermal state
$ sudo powermetrics --samplers thermal -n 1

# If thermal level > 0, system is throttling
# Solutions:
# - Move to cooler environment
# - Check for blocked vents
# - Use a cooling pad
# - Reduce workload

Investigating Spotlight

# Check if Spotlight is indexing
$ sudo mdutil -s /
/System/Volumes/Data:
    Indexing enabled.
    Scan in progress.  <-- Active indexing

# Check mds activity
$ sudo fs_usage mds_stores 2>/dev/null | head -20

# If problematic, disable temporarily
$ sudo mdutil -i off /

# Or exclude specific folders
$ sudo mdutil -E /Volumes/ExternalDrive

# Rebuild index (nuclear option)
$ sudo mdutil -E /

Memory Troubleshooting

Symptoms

  • “Your system has run out of application memory”
  • Extreme slowness
  • Apps crashing
  • Beach ball when switching apps
  • High swap usage

Diagnostic Steps

# Step 1: Check memory pressure
$ memory_pressure
System-wide memory free percentage: 12%
System memory pressure level: 2  # Warning level

# Step 2: Check detailed memory stats
$ vm_stat

# Step 3: Check swap usage
$ sysctl vm.swapusage
vm.swapusage: total = 2048.00M  used = 1500.00M  free = 548.00M

# Step 4: Find memory hogs
$ ps aux --sort=-%mem | head -10

# Step 5: Check specific process memory
$ vmmap --summary $(pgrep Safari) | head -20

Memory Pressure Interpretation

# Run memory_pressure and interpret
$ memory_pressure
System-wide memory free percentage: 45%
System memory pressure level: 1

# Level meanings:
# 1 (Normal): >25% free, no action needed
# 2 (Warning): 5-25% free, close unused apps
# 4 (Critical): <5% free, urgent action needed

Finding Memory Leaks

# Monitor memory growth over time
$ while true; do
    RSS=$(ps -o rss= -p PID)
    echo "$(date +%H:%M:%S) RSS: $((RSS/1024)) MB"
    sleep 60
done

# Growing steadily = probable memory leak

# Use leaks tool (if debug symbols available)
$ leaks PID

# Use Instruments for detailed analysis
# (Xcode > Open Developer Tool > Instruments > Leaks)

Addressing Memory Pressure

# Immediate relief:
# 1. Close unused applications
# 2. Close browser tabs

# Check what's using memory
$ ps aux --sort=-%mem | head -10

# Restart memory-heavy apps
$ killall Safari  # Safari will reopen

# Clear file caches (helps a little)
$ sudo purge

# If chronic issue, consider:
# - More RAM (if upgradeable)
# - Reducing number of concurrent apps
# - Using lighter alternatives (Safari vs Chrome)

Disk I/O Troubleshooting

Symptoms

  • Slow app launches
  • Spinning beach ball
  • Sluggish file operations
  • System hangs during saves

Diagnostic Steps

# Step 1: Check disk activity
$ iostat -d -w 2
              disk0
    KB/t  tps  MB/s
   48.00  456  21.3  <-- High activity

# Step 2: Find I/O sources
$ sudo fs_usage -f diskio 2>/dev/null | head -50

# Step 3: Check disk space
$ df -h /
Filesystem     Size   Used  Avail Capacity
/dev/disk3s1  460Gi  450Gi   10Gi    98%  <-- Very full!

# Step 4: Check for Spotlight indexing
$ sudo fs_usage mds_stores 2>/dev/null

# Step 5: Verify disk health
$ diskutil verifyVolume /

Common Disk Issues

IssueCauseSolution
Disk nearly fullInsufficient spaceFree up space
Constant disk activitySpotlight/Time MachineWait or exclude
Slow read/writeDisk failingRun diagnostics
Beach ball on saveDisk I/O blockedCheck for hangs

Freeing Disk Space

# Check what's using space
$ sudo du -sh /* 2>/dev/null | sort -h | tail -20

# Find large files
$ find ~ -type f -size +500M -exec ls -lh {} \; 2>/dev/null

# Clear system caches
$ sudo rm -rf /Library/Caches/*
$ rm -rf ~/Library/Caches/*

# Empty Trash
$ rm -rf ~/.Trash/*

# Remove old iOS backups
$ ls -la ~/Library/Application\ Support/MobileSync/Backup/

# Clear Xcode derived data
$ rm -rf ~/Library/Developer/Xcode/DerivedData/*

# Remove local Time Machine snapshots
$ tmutil listlocalsnapshots /
$ sudo tmutil deletelocalsnapshots YYYY-MM-DD-HHMMSS

Disk Health Check

# Verify filesystem
$ diskutil verifyVolume /

# First Aid (repairs issues)
$ diskutil repairVolume /

# If issues persist, boot to Recovery Mode (Cmd+R on Intel, hold power on AS)
# and run Disk Utility First Aid on the volume

Network Troubleshooting

Symptoms

  • Slow downloads
  • Web pages load slowly
  • Network requests hang
  • High latency

Diagnostic Steps

# Step 1: Check network connectivity
$ ping -c 5 8.8.8.8

# Step 2: Check DNS
$ dig google.com

# Step 3: Check bandwidth
$ networkQuality
==== SUMMARY ====
Uplink: 45.234 Mbps
Downlink: 120.456 Mbps
Responsiveness: 234 RPM

# Step 4: Find network-heavy processes
$ nettop -P -L 1

# Step 5: Check for connection issues
$ netstat -an | grep ESTABLISHED | wc -l

Common Network Issues

# Flush DNS cache
$ sudo dscacheutil -flushcache
$ sudo killall -HUP mDNSResponder

# Restart network services
$ sudo ifconfig en0 down
$ sudo ifconfig en0 up

# Check Wi-Fi issues
$ /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I

# Renew DHCP lease
$ sudo ipconfig set en0 DHCP

Graphics/UI Troubleshooting

Symptoms

  • Choppy scrolling
  • Laggy animations
  • Screen tearing
  • High WindowServer CPU

Diagnostic Steps

# Check WindowServer CPU
$ top -l 1 | grep WindowServer

# Check GPU usage
$ sudo powermetrics --samplers gpu_power -n 1

# On Intel Macs with discrete GPU, check which is active
$ system_profiler SPDisplaysDataType | grep -E "Chipset|VRAM"

Graphics Fixes

# Reduce visual effects
# System Preferences > Accessibility > Display
# - Reduce motion
# - Reduce transparency

# Via defaults
$ defaults write com.apple.universalaccess reduceMotion -bool true
$ defaults write com.apple.universalaccess reduceTransparency -bool true

# Restart WindowServer (logs you out!)
$ sudo killall -HUP WindowServer

Application-Specific Issues

App Won’t Launch

# Check app signature
$ codesign -v /Applications/Problem.app

# Check quarantine
$ xattr /Applications/Problem.app

# Check console for errors
$ log show --predicate 'process == "Problem"' --last 5m

# Try launching from Terminal
$ /Applications/Problem.app/Contents/MacOS/Problem

App Crashes Repeatedly

# Find crash logs
$ ls -lt ~/Library/Logs/DiagnosticReports/*.crash | head -5

# Read recent crash
$ cat ~/Library/Logs/DiagnosticReports/Problem*.crash | head -100

# Clear app preferences
$ rm ~/Library/Preferences/com.example.problem.plist

# Clear app caches
$ rm -rf ~/Library/Caches/com.example.problem

App Using Too Many Resources

# Identify resource usage
$ ps aux | grep "Problem"
$ vmmap --summary $(pgrep Problem)

# Sample for analysis
$ sample Problem 10 -file /tmp/problem-sample.txt

# Consider alternatives or report to developer

Systematic Troubleshooting Script

#!/bin/bash
# full-diagnostic.sh - Comprehensive performance diagnostic

OUTPUT_DIR="/tmp/mac-diagnostic-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$OUTPUT_DIR"

echo "Running full diagnostic. Output: $OUTPUT_DIR"
echo ""

# System info
echo "Gathering system info..."
sw_vers > "$OUTPUT_DIR/system-version.txt"
sysctl hw > "$OUTPUT_DIR/hardware.txt"
uname -a >> "$OUTPUT_DIR/system-version.txt"

# CPU
echo "Gathering CPU info..."
top -l 3 -n 10 > "$OUTPUT_DIR/top.txt"
ps aux --sort=-%cpu | head -50 > "$OUTPUT_DIR/top-cpu-processes.txt"

# Memory
echo "Gathering memory info..."
vm_stat > "$OUTPUT_DIR/vm_stat.txt"
memory_pressure > "$OUTPUT_DIR/memory_pressure.txt" 2>&1
sysctl vm.swapusage >> "$OUTPUT_DIR/vm_stat.txt"
ps aux --sort=-%mem | head -50 > "$OUTPUT_DIR/top-mem-processes.txt"

# Disk
echo "Gathering disk info..."
df -h > "$OUTPUT_DIR/disk-space.txt"
diskutil list >> "$OUTPUT_DIR/disk-space.txt"
iostat -d -c 5 > "$OUTPUT_DIR/iostat.txt"

# Network
echo "Gathering network info..."
netstat -an | head -100 > "$OUTPUT_DIR/netstat.txt"
ifconfig > "$OUTPUT_DIR/ifconfig.txt"

# Power
echo "Gathering power info..."
pmset -g > "$OUTPUT_DIR/pmset.txt"
pmset -g assertions >> "$OUTPUT_DIR/pmset.txt"
pmset -g batt >> "$OUTPUT_DIR/pmset.txt"

# Logs
echo "Gathering recent logs..."
log show --predicate 'eventType == logEvent' --last 30m 2>/dev/null | head -1000 > "$OUTPUT_DIR/recent-logs.txt"

# Summary
echo "Creating summary..."
cat > "$OUTPUT_DIR/summary.txt" << EOF
=== Performance Diagnostic Summary ===
Date: $(date)
macOS: $(sw_vers -productVersion)
Model: $(sysctl -n hw.model)
CPU: $(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo "Apple Silicon")
RAM: $(sysctl -n hw.memsize | awk '{print $1/1024/1024/1024 " GB"}')

--- Current State ---
Load Average: $(uptime | awk -F'load averages:' '{print $2}')
Memory Pressure: $(memory_pressure 2>/dev/null | grep "free percentage" | awk '{print $5}')
Disk Usage: $(df -h / | tail -1 | awk '{print $5}')
Battery: $(pmset -g batt | grep -o '[0-9]*%' | head -1)

--- Top CPU Consumers ---
$(ps aux --sort=-%cpu | awk 'NR<=6 {print $3 "% " $11}')

--- Top Memory Consumers ---
$(ps aux --sort=-%mem | awk 'NR<=6 {print $4 "% " $11}')

EOF

echo ""
echo "Diagnostic complete. Output saved to: $OUTPUT_DIR"
echo "Review summary: cat $OUTPUT_DIR/summary.txt"

Common Problems Quick Reference

System Feels Generally Slow

# 1. Check load average vs CPU count
$ uptime
$ sysctl -n hw.ncpu

# If load > 2x CPU count, system is overloaded

# 2. Check memory pressure
$ memory_pressure | head -3

# 3. Check disk space (needs >10% free)
$ df -h /

# 4. Reboot if uptime is very long
$ uptime
$ sudo reboot  # Sometimes helps

Slow After macOS Update

# Spotlight reindexing - wait or:
$ sudo mdutil -s /

# Permissions issues
$ diskutil resetUserPermissions / $(id -u)

# Clear font caches
$ sudo atsutil databases -remove

# Rebuild Launch Services database
$ /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user

Slow After Sleep/Wake

# Check power assertions
$ pmset -g assertions

# Check for hung processes
$ top -o cpu

# Restart problem services
$ sudo killall -9 WindowServer  # Logs you out

When to Seek Further Help

  • Hardware diagnostics show errors (Apple Diagnostics: hold D on boot)
  • Disk Utility shows uncorrectable errors
  • Kernel panics occur regularly
  • Issues persist after clean macOS install
  • SMC reset doesn’t help (Intel Macs)
# Run Apple Diagnostics
# Intel: Restart, hold D
# Apple Silicon: Restart, hold power button, select Options > Diagnostics

# Generate system report for Apple Support
$ sudo sysdiagnose
# Creates archive in /var/tmp/

Summary

Troubleshooting workflow:

  1. Identify symptoms - What feels slow?
  2. Check resources - CPU, memory, disk, network
  3. Find the bottleneck - Which resource is constrained?
  4. Identify the cause - Which process or condition?
  5. Apply the fix - Address root cause
  6. Verify resolution - Confirm improvement

Essential diagnostic commands:

ResourceQuick CheckDetailed
CPUtop -l 1 -n 0sample PID
Memorymemory_pressurevmmap PID
Diskiostat -dsudo fs_usage
Networknettop -Pnetstat -an
Powerpmset -g battsudo powermetrics
Overalluptimesysdiagnose

Systematic diagnosis leads to effective solutions, while random fixes often mask the real problem.

Appendix A: Command Reference

This appendix provides a quick reference to essential commands for macOS. Commands are organized by category for easy lookup. For detailed information on any command, use man command-name.

Legend

  • $ - Run as regular user
  • # - Requires sudo (administrator privileges)
  • [optional] - Optional parameter
  • <required> - Required parameter

File and Directory Operations

CommandDescriptionExample
pwdPrint working directory$ pwd
cd <dir>Change directory$ cd ~/Documents
cd -Return to previous directory$ cd -
lsList directory contents$ ls -la
ls -laLong format with hidden files$ ls -la ~/
ls -lahHuman-readable sizes$ ls -lah
ls -ltSort by modification time$ ls -lt
treeDirectory tree (install via Homebrew)$ tree -L 2

File Operations

CommandDescriptionExample
cp <src> <dst>Copy file$ cp file.txt backup.txt
cp -r <src> <dst>Copy directory recursively$ cp -r dir1 dir2
cp -a <src> <dst>Archive mode (preserve attributes)$ cp -a /source /dest
mv <src> <dst>Move/rename file$ mv old.txt new.txt
rm <file>Remove file$ rm unwanted.txt
rm -r <dir>Remove directory recursively$ rm -r old_dir
rm -rf <dir>Force remove (dangerous!)$ rm -rf temp/
mkdir <dir>Create directory$ mkdir newdir
mkdir -p <path>Create nested directories$ mkdir -p a/b/c
rmdir <dir>Remove empty directory$ rmdir emptydir
touch <file>Create file or update timestamp$ touch newfile.txt
ln -s <target> <link>Create symbolic link$ ln -s /path/to/file link
ditto <src> <dst>Copy preserving macOS metadata$ ditto src dst

File Information

CommandDescriptionExample
file <file>Determine file type$ file document.pdf
stat <file>Display file status$ stat file.txt
du -sh <path>Disk usage summary$ du -sh ~/Downloads
du -h -d 1Disk usage one level deep$ du -h -d 1 ~/
wc -l <file>Count lines in file$ wc -l file.txt
mdls <file>View Spotlight metadata$ mdls photo.jpg

Permissions

CommandDescriptionExample
chmod <mode> <file>Change permissions$ chmod 755 script.sh
chmod +x <file>Make executable$ chmod +x run.sh
chmod -R <mode> <dir>Recursive permissions$ chmod -R 644 docs/
chown <user> <file>Change owner# chown admin file.txt
chown -R <user>:<group>Recursive ownership# chown -R www:www html/
chgrp <group> <file>Change group$ chgrp staff file.txt
ls -leList with ACLs$ ls -le
chmod +a <acl>Add ACL entry$ chmod +a "user:joe:allow read" file

Text Processing

Viewing Files

CommandDescriptionExample
cat <file>Display file contents$ cat file.txt
cat -n <file>Display with line numbers$ cat -n script.sh
head <file>First 10 lines$ head file.txt
head -n 20 <file>First N lines$ head -n 20 log.txt
tail <file>Last 10 lines$ tail file.txt
tail -n 50 <file>Last N lines$ tail -n 50 log.txt
tail -f <file>Follow file (live updates)$ tail -f /var/log/system.log
less <file>Page through file$ less largefile.txt
more <file>Simple pager$ more file.txt

Searching

CommandDescriptionExample
grep <pattern> <file>Search for pattern$ grep "error" log.txt
grep -i <pattern>Case-insensitive search$ grep -i "warning" file
grep -r <pattern> <dir>Recursive search$ grep -r "TODO" src/
grep -n <pattern>Show line numbers$ grep -n "func" code.py
grep -v <pattern>Invert match (exclude)$ grep -v "debug" log
grep -E <regex>Extended regex$ grep -E "err(or)?s?" log
grep -l <pattern>List matching files only$ grep -l "main" *.c
grep -c <pattern>Count matches$ grep -c "error" log.txt
mdfind <query>Spotlight search$ mdfind "project report"
mdfind -name <name>Search by filename$ mdfind -name "config.yaml"

Text Manipulation

CommandDescriptionExample
sort <file>Sort lines$ sort names.txt
sort -n <file>Numeric sort$ sort -n numbers.txt
sort -r <file>Reverse sort$ sort -r file.txt
sort -u <file>Sort and remove duplicates$ sort -u list.txt
uniqRemove adjacent duplicates$ sort file | uniq
uniq -cCount occurrences$ sort file | uniq -c
cut -d: -f1Extract field$ cut -d: -f1 /etc/passwd
cut -c1-10Extract characters$ cut -c1-10 file.txt
tr 'a-z' 'A-Z'Translate characters$ echo "hi" | tr 'a-z' 'A-Z'
tr -d '\r'Delete characters$ tr -d '\r' < file.txt
sed 's/old/new/'Substitute first match$ sed 's/foo/bar/' file
sed 's/old/new/g'Substitute all matches$ sed 's/foo/bar/g' file
awk '{print $1}'Print first field$ awk '{print $1}' file
awk -F: '{print $1}'Custom delimiter$ awk -F: '{print $1}' passwd

Comparison

CommandDescriptionExample
diff <file1> <file2>Compare files$ diff old.txt new.txt
diff -u <f1> <f2>Unified diff format$ diff -u old new
diff -r <dir1> <dir2>Compare directories$ diff -r src1 src2
comm <f1> <f2>Compare sorted files$ comm list1 list2
cmp <f1> <f2>Byte-by-byte compare$ cmp file1 file2

Finding Files

CommandDescriptionExample
find <path> -name <pattern>Find by name$ find . -name "*.txt"
find <path> -type fFind files only$ find . -type f
find <path> -type dFind directories only$ find /var -type d
find <path> -mtime -7Modified in last 7 days$ find . -mtime -7
find <path> -size +100MFiles larger than 100MB$ find . -size +100M
find <path> -emptyFind empty files/dirs$ find . -empty
find <path> -exec cmd {} \;Execute command on results$ find . -name "*.log" -exec rm {} \;
locate <pattern>Search locate database$ locate nginx.conf
which <command>Show command path$ which python
whereis <command>Locate binary/man/source$ whereis ls
type <command>Describe command type$ type cd

Archiving and Compression

CommandDescriptionExample
tar -cvf <archive> <files>Create tar archive$ tar -cvf backup.tar dir/
tar -xvf <archive>Extract tar archive$ tar -xvf backup.tar
tar -czvf <archive> <files>Create gzipped tar$ tar -czvf backup.tar.gz dir/
tar -xzvf <archive>Extract gzipped tar$ tar -xzvf backup.tar.gz
tar -cjvf <archive> <files>Create bzip2 tar$ tar -cjvf backup.tar.bz2 dir/
tar -xjvf <archive>Extract bzip2 tar$ tar -xjvf backup.tar.bz2
tar -tvf <archive>List tar contents$ tar -tvf backup.tar
gzip <file>Compress with gzip$ gzip file.txt
gunzip <file.gz>Decompress gzip$ gunzip file.txt.gz
zip -r <archive> <dir>Create zip archive$ zip -r archive.zip dir/
unzip <archive>Extract zip archive$ unzip archive.zip
unzip -l <archive>List zip contents$ unzip -l archive.zip

Process Management

Viewing Processes

CommandDescriptionExample
ps auxList all processes$ ps aux
ps aux | grep <name>Find specific process$ ps aux | grep nginx
pgrep <name>Get PIDs by name$ pgrep -l Safari
topInteractive process viewer$ top
htopEnhanced top (install via Homebrew)$ htop
Activity MonitorGUI process managerOpen from Spotlight

Process Control

CommandDescriptionExample
kill <PID>Terminate process$ kill 1234
kill -9 <PID>Force kill process$ kill -9 1234
killall <name>Kill by name$ killall Safari
pkill <pattern>Kill by pattern$ pkill -f "python script"
<command> &Run in background$ long_task &
jobsList background jobs$ jobs
fgBring to foreground$ fg %1
bgContinue in background$ bg %1
nohup <cmd> &Run immune to hangups$ nohup ./script.sh &
Ctrl+ZSuspend current process
Ctrl+CInterrupt current process

Resource Usage

CommandDescriptionExample
vm_statVirtual memory statistics$ vm_stat
iostatI/O statistics$ iostat
fs_usageFilesystem activity# fs_usage -f filesys
sample <PID> <secs>Sample process$ sample Safari 5

System Information

CommandDescriptionExample
uname -aSystem information$ uname -a
sw_versmacOS version$ sw_vers
system_profilerDetailed system info$ system_profiler SPHardwareDataType
hostnameShow hostname$ hostname
whoamiCurrent username$ whoami
idUser ID and groups$ id
uptimeSystem uptime$ uptime
dateCurrent date/time$ date
calCalendar$ cal
df -hDisk free space$ df -h
diskutil listList disks and partitions$ diskutil list
diskutil info <disk>Disk information$ diskutil info disk0
sysctl -aAll kernel parameters$ sysctl -a
sysctl hw.memsizeTotal RAM$ sysctl hw.memsize
sysctl machdep.cpuCPU information$ sysctl machdep.cpu
ioreg -lIOKit registry$ ioreg -l
nvram -pNVRAM variables$ nvram -p

Network Commands

Network Information

CommandDescriptionExample
ifconfigNetwork interface config$ ifconfig
ifconfig en0Specific interface$ ifconfig en0
ipconfig getifaddr en0Get IP address$ ipconfig getifaddr en0
networksetup -listallnetworkservicesList services$ networksetup -listallnetworkservices
networksetup -getinfo "Wi-Fi"Service info$ networksetup -getinfo "Wi-Fi"
scutil --dnsDNS configuration$ scutil --dns
scutil --proxyProxy configuration$ scutil --proxy
netstat -anNetwork connections$ netstat -an
netstat -rnRouting table$ netstat -rn
lsof -iOpen network connections$ lsof -i
lsof -i :80Connections on port 80$ lsof -i :80

Connectivity Testing

CommandDescriptionExample
ping <host>Test connectivity$ ping apple.com
ping -c 5 <host>Send 5 pings$ ping -c 5 google.com
traceroute <host>Trace route to host$ traceroute apple.com
mtr <host>Combined ping/traceroute$ mtr google.com
curl <url>Transfer URL$ curl https://api.example.com
curl -I <url>Fetch headers only$ curl -I https://apple.com
curl -o <file> <url>Download to file$ curl -o file.zip https://url
wget <url>Download file (Homebrew)$ wget https://example.com/file
nc -zv <host> <port>Test port connectivity$ nc -zv google.com 443
nslookup <host>DNS lookup$ nslookup apple.com
dig <host>DNS lookup (detailed)$ dig apple.com
host <host>DNS lookup (simple)$ host apple.com
arp -aARP table$ arp -a

Wi-Fi

CommandDescriptionExample
networksetup -setairportpower en0 onTurn Wi-Fi on$ networksetup -setairportpower en0 on
networksetup -setairportpower en0 offTurn Wi-Fi off$ networksetup -setairportpower en0 off
networksetup -getairportnetwork en0Current network$ networksetup -getairportnetwork en0

Disk and Volume Management

CommandDescriptionExample
diskutil listList all disks$ diskutil list
diskutil info <disk>Disk information$ diskutil info disk0
diskutil mount <vol>Mount volume$ diskutil mount disk2s1
diskutil unmount <vol>Unmount volume$ diskutil unmount disk2s1
diskutil eject <disk>Eject disk$ diskutil eject disk2
diskutil verifyVolume <vol>Verify filesystem# diskutil verifyVolume /
diskutil repairVolume <vol>Repair filesystem# diskutil repairVolume disk1
diskutil apfs listList APFS volumes$ diskutil apfs list
hdiutil attach <dmg>Mount disk image$ hdiutil attach image.dmg
hdiutil detach <vol>Detach disk image$ hdiutil detach /Volumes/Image
hdiutil createCreate disk image$ hdiutil create -size 1g -fs APFS -volname "MyDisk" disk.dmg
tmutilTime Machine utility$ tmutil listbackups

User Management

CommandDescriptionExample
whoamiCurrent user$ whoami
idUser/group IDs$ id
groupsGroup memberships$ groups
dscl . -list /UsersList all users$ dscl . -list /Users
dscl . -list /GroupsList all groups$ dscl . -list /Groups
dscl . -read /Users/<user>User details$ dscl . -read /Users/admin
dscacheutil -q userQuery user cache$ dscacheutil -q user
sysadminctl -addUserAdd user# sysadminctl -addUser joe -password pass
sysadminctl -deleteUserDelete user# sysadminctl -deleteUser joe
sudo -sRoot shell$ sudo -s
su - <user>Switch user$ su - otheruser

Service and Process Management

launchctl

CommandDescriptionExample
launchctl listList loaded services$ launchctl list
launchctl list | grep <name>Find service$ launchctl list | grep apache
launchctl load <plist>Load service (legacy)$ launchctl load ~/Library/LaunchAgents/job.plist
launchctl unload <plist>Unload service (legacy)$ launchctl unload ~/Library/LaunchAgents/job.plist
launchctl bootstrap gui/<uid> <plist>Bootstrap service$ launchctl bootstrap gui/501 job.plist
launchctl bootout gui/<uid> <plist>Remove service$ launchctl bootout gui/501 job.plist
launchctl kickstartStart service$ launchctl kickstart gui/501/com.example.agent
launchctl kill <sig> <service>Send signal# launchctl kill SIGTERM system/com.example.daemon
launchctl print systemPrint system domain$ launchctl print system
launchctl print gui/501Print user domain$ launchctl print gui/501

Homebrew Services

CommandDescriptionExample
brew services listList services$ brew services list
brew services start <name>Start service$ brew services start postgresql
brew services stop <name>Stop service$ brew services stop postgresql
brew services restart <name>Restart service$ brew services restart nginx
brew services run <name>Run once (no restart)$ brew services run redis

Package Management (Homebrew)

CommandDescriptionExample
brew install <pkg>Install package$ brew install wget
brew uninstall <pkg>Uninstall package$ brew uninstall wget
brew upgradeUpgrade all packages$ brew upgrade
brew upgrade <pkg>Upgrade specific package$ brew upgrade node
brew updateUpdate Homebrew$ brew update
brew search <term>Search packages$ brew search python
brew info <pkg>Package information$ brew info python
brew listList installed packages$ brew list
brew list --caskList installed casks$ brew list --cask
brew outdatedShow outdated packages$ brew outdated
brew cleanupRemove old versions$ brew cleanup
brew doctorDiagnose issues$ brew doctor
brew deps <pkg>Show dependencies$ brew deps node
brew uses --installed <pkg>What uses this package$ brew uses --installed openssl
brew install --cask <app>Install GUI app$ brew install --cask firefox

macOS-Specific Commands

Clipboard

CommandDescriptionExample
pbcopyCopy to clipboard$ cat file.txt | pbcopy
pbpastePaste from clipboard$ pbpaste > output.txt
pbcopy < file.txtCopy file contents$ pbcopy < ~/.ssh/id_rsa.pub

Launching and Opening

CommandDescriptionExample
open <file>Open with default app$ open document.pdf
open -a <app> <file>Open with specific app$ open -a "Visual Studio Code" .
open .Open current dir in Finder$ open .
open <url>Open URL in browser$ open https://apple.com
open -R <file>Reveal in Finder$ open -R file.txt

System

CommandDescriptionExample
say <text>Text-to-speech$ say "Hello world"
screencaptureTake screenshot$ screencapture screen.png
screencapture -cScreenshot to clipboard$ screencapture -c
pmset -gPower management status$ pmset -g
pmset -g battBattery status$ pmset -g batt
caffeinatePrevent sleep$ caffeinate -t 3600
softwareupdate -lList software updates$ softwareupdate -l
softwareupdate -iaInstall all updates# softwareupdate -ia
defaults readRead preferences$ defaults read com.apple.finder
defaults writeWrite preferences$ defaults write com.apple.finder ShowHiddenFiles -bool true
osascript -eRun AppleScript$ osascript -e 'display notification "Done"'

Spotlight

CommandDescriptionExample
mdfind <query>Spotlight search$ mdfind "meeting notes"
mdfind -name <name>Search by name$ mdfind -name "config.yaml"
mdfind -onlyin <dir> <query>Search in directory$ mdfind -onlyin ~/Documents "report"
mdls <file>File metadata$ mdls photo.jpg
mdutil -s /Spotlight status$ mdutil -s /
mdutil -E /Rebuild Spotlight index# mdutil -E /

Logging and Debugging

CommandDescriptionExample
log showView unified log$ log show --last 1h
log show --predicateFilter logs$ log show --predicate 'process == "Safari"'
log streamReal-time log stream$ log stream --level debug
consoleOpen Console.app$ open -a Console
syslogLegacy system log (deprecated)$ syslog
dmesgKernel messages$ dmesg
system_profiler SPLogsDataTypeSystem logs info$ system_profiler SPLogsDataType

Security Commands

CommandDescriptionExample
csrutil statusSIP status$ csrutil status
spctl --statusGatekeeper status$ spctl --status
spctl -a -v <app>Verify app signature$ spctl -a -v /Applications/Safari.app
codesign -dv <app>Code signature details$ codesign -dv /Applications/Safari.app
codesign -s <identity> <file>Sign code$ codesign -s "Developer ID" myapp
security list-keychainsList keychains$ security list-keychains
security find-identity -vList signing identities$ security find-identity -v
security find-generic-passwordFind password$ security find-generic-password -s "service"
fdesetup statusFileVault status$ fdesetup status
sudo spctl --master-disableDisable Gatekeeper# spctl --master-disable

Remote Access

CommandDescriptionExample
ssh <user>@<host>SSH connection$ ssh admin@server.local
ssh -i <key> <user>@<host>SSH with key$ ssh -i ~/.ssh/mykey user@host
ssh -L <local>:<remote>SSH tunnel$ ssh -L 8080:localhost:80 user@host
scp <src> <user>@<host>:<dst>Secure copy$ scp file.txt user@host:/path/
scp -r <dir> <user>@<host>:<dst>Recursive copy$ scp -r dir/ user@host:/path/
rsync -avz <src> <dst>Sync files$ rsync -avz dir/ user@host:/path/
sftp <user>@<host>SFTP session$ sftp admin@server.local

Shell Built-ins and History

CommandDescriptionExample
historyShow command history$ history
history | grep <term>Search history$ history | grep git
!!Repeat last command$ !!
!<n>Run command N from history$ !42
!<string>Run last command starting with$ !git
Ctrl+RReverse search history
aliasShow aliases$ alias
alias <name>=<cmd>Create alias$ alias ll='ls -la'
unalias <name>Remove alias$ unalias ll
export <var>=<val>Set environment variable$ export PATH="/usr/local/bin:$PATH"
envShow environment$ env
printenvPrint environment variables$ printenv
echo $<var>Print variable$ echo $HOME

Getting Help

CommandDescriptionExample
man <command>Manual page$ man ls
man -k <term>Search manual pages$ man -k network
apropos <term>Same as man -k$ apropos copy
<command> --helpCommand help$ git --help
<command> -hShort help$ grep -h
whatis <command>One-line description$ whatis curl
info <command>GNU info pages$ info bash

Quick Reference: Keyboard Shortcuts in Commands

ShortcutAction
Ctrl+CCancel/interrupt
Ctrl+DEOF/logout
Ctrl+ZSuspend process
Ctrl+LClear screen
Ctrl+ABeginning of line
Ctrl+EEnd of line
Ctrl+UClear line before cursor
Ctrl+KClear line after cursor
Ctrl+WDelete word before cursor
Ctrl+RReverse search history
TabAuto-complete
Tab TabShow completions

Appendix B: Configuration File Locations

This appendix documents where configuration files are located on macOS, how they differ from Linux conventions, and where to find common settings.


macOS Directory Structure Overview

macOS uses a hybrid filesystem hierarchy combining traditional Unix paths with Apple-specific locations:

/
├── Applications/              # GUI applications
├── Library/                   # System-wide resources and preferences
├── System/                    # macOS system files (read-only with SIP)
├── Users/                     # User home directories
│   └── <username>/
│       ├── Applications/      # User-installed apps
│       ├── Desktop/
│       ├── Documents/
│       ├── Downloads/
│       ├── Library/           # User-specific preferences and data
│       └── ...
├── Volumes/                   # Mounted volumes
├── bin/                       # Essential user commands
├── etc/                       # System configuration (symlink to /private/etc)
├── private/                   # Actual location of /etc, /var, /tmp
├── sbin/                      # Essential system commands
├── tmp/                       # Temporary files (symlink)
├── usr/                       # Secondary hierarchy
│   ├── bin/                   # User commands
│   ├── lib/                   # Libraries
│   ├── local/                 # Locally installed software
│   └── share/                 # Architecture-independent data
└── var/                       # Variable data (symlink to /private/var)

Shell Configuration Files

Zsh (Default Shell)

FileScopePurpose
/etc/zshenvSystemRead for all zsh invocations
/etc/zprofileSystemRead for login shells
/etc/zshrcSystemRead for interactive shells
/etc/zloginSystemRead after zprofile for login shells
~/.zshenvUserUser environment (all invocations)
~/.zprofileUserUser login shell setup
~/.zshrcUserUser interactive shell config
~/.zloginUserUser post-login commands
~/.zlogoutUserLogout cleanup

Load order for interactive login shell:

  1. /etc/zshenv
  2. ~/.zshenv
  3. /etc/zprofile
  4. ~/.zprofile
  5. /etc/zshrc
  6. ~/.zshrc
  7. /etc/zlogin
  8. ~/.zlogin

Bash

FileScopePurpose
/etc/profileSystemLogin shell initialization
/etc/bashrcSystemNon-login interactive shells
~/.bash_profileUserLogin shell (read instead of .profile)
~/.bash_loginUserLogin shell (fallback)
~/.profileUserLogin shell (fallback if no .bash_profile)
~/.bashrcUserInteractive non-login shells
~/.bash_logoutUserLogout cleanup

Note: On macOS, Terminal.app opens login shells by default, so ~/.bash_profile is used rather than ~/.bashrc. Many users source .bashrc from .bash_profile for consistency.

Shell-Agnostic

FilePurpose
~/.inputrcGNU Readline configuration
/etc/pathsSystem PATH directories (one per line)
/etc/paths.d/*Additional PATH directories
/etc/manpathsManual page paths
/etc/manpaths.d/*Additional manual paths

Application Preferences (Property Lists)

macOS applications store preferences in property list (plist) files:

User Preferences

LocationContents
~/Library/Preferences/User app preferences (.plist files)
~/Library/Preferences/ByHost/Machine-specific user preferences

Common preference files:

~/Library/Preferences/
├── com.apple.finder.plist           # Finder settings
├── com.apple.dock.plist             # Dock settings
├── com.apple.Terminal.plist         # Terminal.app settings
├── com.apple.Safari.plist           # Safari settings
├── .GlobalPreferences.plist         # Global user defaults
├── com.googlecode.iterm2.plist      # iTerm2 settings
└── com.microsoft.VSCode.plist       # VS Code settings

System Preferences

LocationContents
/Library/Preferences/System-wide app preferences
/Library/Preferences/SystemConfiguration/Network and system config

Reading and Modifying Preferences

# Read all preferences for an app
$ defaults read com.apple.finder

# Read specific key
$ defaults read com.apple.finder ShowHardDrivesOnDesktop

# Write a preference
$ defaults write com.apple.finder ShowHardDrivesOnDesktop -bool true

# Delete a preference (revert to default)
$ defaults delete com.apple.finder ShowHardDrivesOnDesktop

# Export plist to XML (readable)
$ plutil -convert xml1 -o - ~/Library/Preferences/com.apple.finder.plist

# List all domains
$ defaults domains | tr ',' '\n'

Launch Agents and Daemons

Directories by Scope

LocationRuns AsLoaded When
~/Library/LaunchAgents/Current userUser logs in
/Library/LaunchAgents/Current userUser logs in
/System/Library/LaunchAgents/Current userUser logs in (Apple only)
/Library/LaunchDaemons/root (or specified user)System boot
/System/Library/LaunchDaemons/rootSystem boot (Apple only)

Example LaunchAgent Locations

~/Library/LaunchAgents/
├── com.example.myagent.plist        # User-installed agent
├── homebrew.mxcl.postgresql.plist   # Homebrew service
└── com.docker.helper.plist          # Docker helper

/Library/LaunchDaemons/
├── com.example.mydaemon.plist       # Third-party daemon
├── com.docker.vmnetd.plist          # Docker networking daemon
└── org.postgresql.postgres.plist    # PostgreSQL daemon

Managing Services

# List user agents
$ launchctl list

# Load an agent
$ launchctl load ~/Library/LaunchAgents/com.example.agent.plist

# Unload an agent
$ launchctl unload ~/Library/LaunchAgents/com.example.agent.plist

# Bootstrap (modern method)
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.agent.plist

# Bootout (modern method)
$ launchctl bootout gui/$(id -u)/com.example.agent

Application Support and Data

User Application Data

LocationContents
~/Library/Application Support/App data, caches, state
~/Library/Caches/Cached data (safe to delete)
~/Library/Logs/Application logs
~/Library/Containers/Sandboxed app data
~/Library/Group Containers/Shared data between related apps
~/Library/Saved Application State/Window positions, open documents

Common Application Support paths:

~/Library/Application Support/
├── Code/                    # VS Code
├── Firefox/                 # Firefox profiles
├── Google/Chrome/           # Chrome profiles
├── Slack/                   # Slack data
├── iTerm2/                  # iTerm2 data
├── JetBrains/               # IntelliJ, PyCharm, etc.
└── com.apple.TCC/           # Privacy database

System Application Data

LocationContents
/Library/Application Support/System-wide app data
/Library/Caches/System caches
/Library/Logs/System and app logs

Development Tools

Xcode and Command Line Tools

LocationContents
/Applications/Xcode.app/Xcode IDE
/Library/Developer/CommandLineTools/CLI tools (headers, compilers)
~/Library/Developer/User developer data
~/Library/Developer/Xcode/Xcode user data
~/Library/Developer/Xcode/DerivedData/Build products
/Applications/Xcode.app/Contents/Developer/Platforms/SDKs

SDK and Header Locations

# Find active developer directory
$ xcode-select -p
/Library/Developer/CommandLineTools

# Find SDK path
$ xcrun --show-sdk-path
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk

# SDK locations
/Library/Developer/CommandLineTools/SDKs/
├── MacOSX.sdk -> MacOSX14.sdk
├── MacOSX14.sdk/
│   └── usr/include/        # System headers
└── MacOSX13.sdk/

Homebrew

Location (Apple Silicon)Location (Intel)Contents
/opt/homebrew//usr/local/Homebrew prefix
/opt/homebrew/bin//usr/local/bin/Executable symlinks
/opt/homebrew/Cellar//usr/local/Cellar/Installed packages
/opt/homebrew/Caskroom//usr/local/Caskroom/Installed casks
/opt/homebrew/etc//usr/local/etc/Configuration files
/opt/homebrew/var//usr/local/var/Variable data
# Find Homebrew prefix
$ brew --prefix
/opt/homebrew

# Find package location
$ brew --prefix postgresql
/opt/homebrew/opt/postgresql

# Find config file location
$ brew --prefix/etc/nginx/nginx.conf

Language Version Managers

ToolConfigurationVersions Location
pyenv~/.pyenv/~/.pyenv/versions/
rbenv~/.rbenv/~/.rbenv/versions/
nvm~/.nvm/~/.nvm/versions/
nodenv~/.nodenv/~/.nodenv/versions/
rustup~/.rustup/~/.rustup/toolchains/

Network Configuration

DNS

LocationPurpose
/etc/resolv.confDNS resolver configuration (often managed by macOS)
/etc/hostsStatic hostname mappings
/Library/Preferences/SystemConfiguration/preferences.plistNetwork service configuration
# View DNS configuration
$ scutil --dns

# View network configuration
$ cat /Library/Preferences/SystemConfiguration/preferences.plist | plutil -convert xml1 -o - -

SSH

LocationPurpose
~/.ssh/configUser SSH client configuration
~/.ssh/known_hostsKnown host keys
~/.ssh/authorized_keysKeys allowed to log in as you
~/.ssh/id_rsa, ~/.ssh/id_ed25519Private keys
~/.ssh/id_rsa.pub, ~/.ssh/id_ed25519.pubPublic keys
/etc/ssh/ssh_configSystem-wide SSH client config
/etc/ssh/sshd_configSSH server configuration

Firewall

LocationPurpose
/etc/pf.confPacket Filter configuration
/etc/pf.anchors/PF anchor rules
/Library/Preferences/com.apple.alf.plistApplication Layer Firewall settings

Security

Keychain

LocationPurpose
~/Library/Keychains/login.keychain-dbUser login keychain
/Library/Keychains/System.keychainSystem keychain
/System/Library/Keychains/SystemRootCertificates.keychainRoot certificates

TCC (Privacy) Database

LocationPurpose
~/Library/Application Support/com.apple.TCC/TCC.dbUser privacy permissions
/Library/Application Support/com.apple.TCC/TCC.dbSystem privacy permissions

Gatekeeper

# Gatekeeper configuration
$ spctl --status
assessments enabled

# Security assessment policy
/var/db/SystemPolicy
/var/db/SystemPolicyConfiguration

Logs

Unified Logging

macOS uses a unified logging system. Logs are stored in a binary format:

LocationPurpose
/var/db/diagnostics/Log data store
/var/db/uuidtext/Log text data
# View recent logs
$ log show --last 1h

# Stream live logs
$ log stream

# Filter by process
$ log show --predicate 'process == "Safari"' --last 30m

# Filter by subsystem
$ log show --predicate 'subsystem == "com.apple.network"' --last 1h

Traditional Log Files

LocationContents
/var/log/system.logGeneral system log (limited)
/var/log/install.logInstallation logs
/var/log/wifi.logWi-Fi diagnostics
~/Library/Logs/User application logs
/Library/Logs/System application logs
/Library/Logs/DiagnosticReports/Crash reports
~/Library/Logs/DiagnosticReports/User crash reports

Comparison: macOS vs Linux

Configuration File Locations

PurposemacOSLinux
Shell config~/.zshrc~/.bashrc, ~/.zshrc
App preferences~/Library/Preferences/*.plist~/.config/, ~/.<app>
App data~/Library/Application Support/~/.local/share/, ~/.<app>
App cache~/Library/Caches/~/.cache/
System services/Library/LaunchDaemons//etc/systemd/system/
User services~/Library/LaunchAgents/~/.config/systemd/user/
Fonts~/Library/Fonts/, /Library/Fonts/~/.fonts/, /usr/share/fonts/
Binaries/usr/local/bin/, /opt/homebrew/bin//usr/local/bin/
Service configEmbedded in .plist/etc/<service>/

Service Configuration

Linux (systemd):

/etc/systemd/system/myservice.service
/etc/myservice/config.conf          # Separate config file

macOS (launchd):

/Library/LaunchDaemons/com.example.myservice.plist
# Configuration often embedded in plist or in:
/Library/Application Support/MyService/config.conf

Path Differences

PurposemacOSLinux
Package manager prefix/opt/homebrew/ (ARM), /usr/local/ (Intel)Varies by distro
System binaries/bin/, /sbin/, /usr/bin/Same (or merged /usr/bin/)
Temp files/private/tmp/ (/tmp/ symlink)/tmp/
Variable data/private/var/ (/var/ symlink)/var/
Optional software/opt//opt/
Mounts/Volumes//mnt/, /media/

Finding Configuration Files

Using Spotlight

# Find all plist files
$ mdfind -name ".plist"

# Find config files in Library
$ mdfind -onlyin ~/Library "kMDItemFSName == '*.conf'"

# Find preference files for an app
$ mdfind "kMDItemContentType == 'com.apple.property-list'" | grep -i appname

Using find

# Find all hidden config files in home
$ find ~ -maxdepth 1 -name ".*" -type f

# Find all plist files in Library
$ find ~/Library -name "*.plist" 2>/dev/null

# Find all configuration directories
$ find ~ -maxdepth 2 -type d -name ".*" 2>/dev/null

Common Locations to Check

When troubleshooting an application, check these locations:

# Preferences
~/Library/Preferences/com.<company>.<app>.plist
~/Library/Preferences/<app>.plist

# Application Support
~/Library/Application Support/<App Name>/

# Caches
~/Library/Caches/<App Name>/
~/Library/Caches/com.<company>.<app>/

# Containers (sandboxed apps)
~/Library/Containers/<bundle-id>/

# Logs
~/Library/Logs/<App Name>/

# Launch Agents (if app has background services)
~/Library/LaunchAgents/com.<company>.<app>.plist

Quick Reference: Key Locations

User Configuration

~/.zshrc                              # Zsh configuration
~/.ssh/config                         # SSH client configuration
~/.gitconfig                          # Git configuration
~/Library/Preferences/                # Application preferences (plist)
~/Library/Application Support/        # Application data
~/Library/LaunchAgents/               # User launch agents

System Configuration

/etc/hosts                            # Host name mappings
/etc/paths                            # System PATH
/etc/shells                           # Valid login shells
/etc/ssh/                             # SSH server configuration
/Library/LaunchDaemons/               # System daemons
/Library/Preferences/                 # System-wide app preferences

Development

/Library/Developer/CommandLineTools/  # Xcode CLI tools
/opt/homebrew/                        # Homebrew (Apple Silicon)
/usr/local/                           # Homebrew (Intel) / local software
~/.pyenv/, ~/.rbenv/, ~/.nvm/         # Language version managers

Homebrew Services

/opt/homebrew/etc/                    # Configuration files
/opt/homebrew/var/                    # Data files
/opt/homebrew/var/log/                # Log files
~/Library/LaunchAgents/homebrew.*     # Service plists

Appendix C: Keyboard Shortcuts

This appendix covers keyboard shortcuts for Terminal.app, command-line editing, and common terminal emulators on macOS.


Terminal.app Shortcuts

Window and Tab Management

ShortcutAction
Cmd+NNew window
Cmd+TNew tab
Cmd+Shift+NNew window with same command
Cmd+WClose tab/window
Cmd+Shift+WClose window
Cmd+1-9Switch to tab 1-9
Cmd+Shift+[Previous tab
Cmd+Shift+]Next tab
Cmd+Left ArrowPrevious tab
Cmd+Right ArrowNext tab
Cmd+Shift+DSplit pane horizontally
Cmd+DSplit pane vertically (some configs)
Cmd+Shift+EnterToggle full screen
Cmd+Ctrl+FToggle full screen

Text and Display

ShortcutAction
Cmd++ or Cmd+=Increase font size
Cmd+-Decrease font size
Cmd+0Reset font size to default
Cmd+KClear screen and scrollback
Cmd+LClear screen (keep scrollback)
Ctrl+LClear screen (shell command)
Cmd+HomeScroll to top
Cmd+EndScroll to bottom
Page UpScroll up one page
Page DownScroll down one page
Cmd+Up ArrowScroll up one line
Cmd+Down ArrowScroll down one line

Selection and Clipboard

ShortcutAction
Cmd+ASelect all
Cmd+CCopy selection
Cmd+VPaste
Cmd+Shift+VPaste escaped (for URLs, paths)
Double-clickSelect word
Triple-clickSelect line
Cmd+ClickOpen URL in browser
Option+ClickPosition cursor at click location
Cmd+DragRectangular selection
ShortcutAction
Cmd+FFind
Cmd+GFind next
Cmd+Shift+GFind previous
Cmd+EUse selection for find
Cmd+JJump to selection

Marks and Bookmarks

ShortcutAction
Cmd+UMark current line
Cmd+Shift+UMark line and send return
Cmd+Shift+MInsert bookmark
Cmd+Up ArrowJump to previous mark
Cmd+Down ArrowJump to next mark
Cmd+Shift+ASelect between marks

Shell Line Editing (Emacs Mode)

Most shells (bash, zsh) use Emacs-style keybindings by default. These work in Terminal.app and other terminal emulators.

Cursor Movement

ShortcutAction
Ctrl+AMove to beginning of line
Ctrl+EMove to end of line
Ctrl+FMove forward one character
Ctrl+BMove backward one character
Option+F or Esc FMove forward one word
Option+B or Esc BMove backward one word
Ctrl+XXToggle between start of line and current position

Note: On macOS, Option+F and Option+B may require enabling “Use Option as Meta key” in Terminal preferences, or use Esc followed by the letter.

Deletion

ShortcutAction
Ctrl+DDelete character under cursor (or logout if empty line)
Ctrl+HDelete character before cursor (backspace)
Ctrl+WDelete word before cursor
Option+D or Esc DDelete word after cursor
Ctrl+UDelete from cursor to beginning of line
Ctrl+KDelete from cursor to end of line
Ctrl+YPaste (yank) last deleted text
Option+YCycle through kill ring

Text Manipulation

ShortcutAction
Ctrl+TTranspose characters (swap current and previous)
Option+T or Esc TTranspose words
Option+U or Esc UUppercase word from cursor
Option+L or Esc LLowercase word from cursor
Option+C or Esc CCapitalize word from cursor

History Navigation

ShortcutAction
Ctrl+P or Up ArrowPrevious command in history
Ctrl+N or Down ArrowNext command in history
Ctrl+RReverse incremental search
Ctrl+SForward incremental search (may need to enable)
Ctrl+GCancel search and restore original line
Option+< or Esc <First command in history
Option+> or Esc >Last command in history
Ctrl+OExecute command and fetch next from history
!!Repeat last command (type and press Enter)
!nRepeat command number n from history
!stringRepeat last command starting with string
!?stringRepeat last command containing string

Process Control

ShortcutAction
Ctrl+CInterrupt (SIGINT) - cancel current command
Ctrl+ZSuspend (SIGTSTP) - pause current command
Ctrl+DEnd of file (EOF) - logout or close input
Ctrl+\Quit (SIGQUIT) - forceful termination
Ctrl+SPause output (XOFF)
Ctrl+QResume output (XON)

Completion

ShortcutAction
TabAuto-complete command, filename, or variable
Tab TabShow all completions
Option+= or Esc =List possible completions
Option+* or Esc *Insert all completions
Option+/ or Esc /Complete filename
Ctrl+X /List possible filename completions

Screen Control

ShortcutAction
Ctrl+LClear screen, redraw current line at top
Ctrl+SStop output to screen
Ctrl+QResume output to screen

Zsh-Specific Shortcuts

Zsh includes additional features beyond standard Emacs bindings:

Zsh Expansion

ShortcutAction
TabComplete and show menu if ambiguous
Ctrl+ISame as Tab
Shift+TabReverse through completions
Option+H or Esc HRun help for current command
Option+?Show command help
Ctrl+X AExpand alias
Ctrl+X GList expansions of current glob
Ctrl+X *Expand glob inline

Zsh History

ShortcutAction
Ctrl+RIncremental history search
Ctrl+P / Ctrl+NNavigate history
Option+PHistory search backward (prefix match)
Option+NHistory search forward (prefix match)
fcEdit last command in editor
rRe-run last command
r foo=barRe-run last command, replacing foo with bar

Zsh Line Editor (ZLE)

ShortcutAction
Ctrl+X Ctrl+EEdit command line in $EDITOR
Option+QPush line to buffer, clear, execute next command, then restore
Option+'Quote line
Option+"Quote region
Ctrl+X Ctrl+VShow zsh version

Vi Mode

Both bash and zsh support vi-style editing. Enable with:

# Bash
set -o vi

# Zsh
bindkey -v

Vi Command Mode (Press Escape first)

KeyAction
hMove left
lMove right
wMove forward one word
bMove backward one word
eMove to end of word
0Move to beginning of line
$Move to end of line
^Move to first non-blank character
xDelete character
dwDelete word
ddDelete line
d$ or DDelete to end of line
d0Delete to beginning of line
cwChange word
ccChange line
c$ or CChange to end of line
yyYank (copy) line
ywYank word
pPaste after cursor
PPaste before cursor
uUndo
Ctrl+RRedo
iInsert mode at cursor
IInsert at beginning of line
aAppend after cursor
AAppend at end of line
rReplace single character
RReplace mode
kPrevious history
jNext history
/Search forward in history
?Search backward in history
nRepeat search
NRepeat search in reverse
vEdit command in $EDITOR

Vi Insert Mode

KeyAction
EscapeReturn to command mode
Ctrl+[Return to command mode
Ctrl+CCancel and return to command mode

iTerm2 Shortcuts

iTerm2 provides additional shortcuts beyond Terminal.app:

Windows and Tabs

ShortcutAction
Cmd+NNew window
Cmd+TNew tab
Cmd+WClose tab
Cmd+Shift+WClose window
Cmd+Option+WClose all tabs except current
Cmd+1-9Switch to tab
Cmd+Left/RightPrevious/next tab
Cmd+Shift+EnterMaximize pane
Cmd+Option+EExpose all tabs

Panes (Split View)

ShortcutAction
Cmd+DSplit vertically
Cmd+Shift+DSplit horizontally
Cmd+Option+ArrowNavigate between panes
Cmd+]Next pane
Cmd+[Previous pane
Cmd+Shift+EnterToggle pane zoom
Cmd+Option+Shift+H/VMove divider

Search and Selection

ShortcutAction
Cmd+FFind
Cmd+Shift+HPaste history
Cmd+;Autocomplete
Cmd+Shift+;Open command history
Cmd+Option+/Recent directories popup
Cmd+ClickOpen URL/file
Cmd+Option+BInstant replay

Text

ShortcutAction
Cmd+KClear buffer
Cmd+Ctrl+KClear scrollback
Cmd+/Find cursor
Cmd+Option+;Open command history
Cmd+Shift+MSet mark
Cmd+Shift+JJump to mark

Profiles and Settings

ShortcutAction
Cmd+IEdit session
Cmd+,Preferences
Cmd+Option+IToggle broadcast input
Cmd+Shift+OOpen quickly (fuzzy search tabs)

Less Pager Shortcuts

When viewing files with less:

KeyAction
Space or fForward one page
bBackward one page
dForward half page
uBackward half page
j or DownForward one line
k or UpBackward one line
g or HomeGo to beginning
G or EndGo to end
/<pattern>Search forward
?<pattern>Search backward
nNext search match
NPrevious search match
&<pattern>Show only matching lines
m<letter>Mark current position
'<letter>Go to mark
FFollow mode (like tail -f)
vOpen in $EDITOR
-NToggle line numbers
-SToggle line wrapping
hHelp
qQuit

Man Page Shortcuts

Man pages use less by default, but some additional keys work:

KeyAction
hHelp
qQuit
SpaceNext page
bPrevious page
/Search
nNext match
NPrevious match

Vim Quick Reference

For quick edits in vim:

Normal Mode

KeyAction
iInsert before cursor
aInsert after cursor
oInsert new line below
OInsert new line above
xDelete character
ddDelete line
yyCopy line
pPaste
uUndo
Ctrl+RRedo
/Search
nNext match
:wSave
:qQuit
:wqSave and quit
:q!Quit without saving
ZZSave and quit
ZQQuit without saving

Quick Reference Card

Essential Shortcuts (Memorize These)

ShortcutAction
Ctrl+CCancel/interrupt
Ctrl+DExit/EOF
Ctrl+ZSuspend
Ctrl+LClear screen
Ctrl+AStart of line
Ctrl+EEnd of line
Ctrl+UDelete to start
Ctrl+KDelete to end
Ctrl+WDelete word
Ctrl+RSearch history
TabAutocomplete
Up/DownHistory navigation

Terminal.app Essentials

ShortcutAction
Cmd+TNew tab
Cmd+WClose tab
Cmd+1-9Switch tab
Cmd+KClear all
Cmd+FFind
Cmd++/-Font size

Process Control

ShortcutAction
Ctrl+CKill foreground
Ctrl+ZSuspend
bgContinue in background
fgBring to foreground
jobsList background jobs

Appendix D: Glossary

This glossary defines key terms related to macOS, Unix, and the command line.


A

ACL (Access Control List)
An extended permission system beyond traditional Unix permissions (user/group/other). ACLs allow fine-grained control over file access for multiple users and groups. On macOS, view with ls -le and modify with chmod +a.
APFS (Apple File System)
Apple’s modern filesystem introduced in 2017, replacing HFS+. Features include native encryption, snapshots, space sharing between volumes, copy-on-write, and crash protection. Optimized for flash/SSD storage.
Apple Silicon
Apple’s custom ARM-based processors for Mac (M1, M2, M3, M4 series). These chips use a different architecture (arm64) than Intel Macs (x86_64), affecting binary compatibility and some system paths.
Application Bundle
A directory structure with a .app extension that contains a macOS application. The bundle includes the executable, resources, frameworks, and metadata in a standardized layout. Appears as a single file in Finder.
ARD (Apple Remote Desktop)
Apple’s remote administration tool for managing Mac computers. Uses the VNC protocol for screen sharing with additional management features.

B

Bash (Bourne Again Shell)
A Unix shell and command language, formerly the default shell on macOS (through Catalina). Replaced by zsh as the default in macOS Catalina (10.15).
Bonjour
Apple’s implementation of zero-configuration networking (mDNS/DNS-SD). Allows automatic discovery of devices and services on a local network without manual configuration. The command dns-sd provides CLI access.
BSD (Berkeley Software Distribution)
A Unix operating system derivative developed at UC Berkeley. macOS’s userland (command-line tools) derives from FreeBSD. Key BSD characteristics include the BSD license, specific command-line tool behaviors, and system call conventions.
Bundle Identifier
A reverse-DNS format string uniquely identifying a macOS application (e.g., com.apple.Safari). Used in preferences, entitlements, code signing, and system services.

C

Catalina
macOS 10.15 (2019), notable for switching the default shell to zsh, introducing a read-only system volume, deprecating 32-bit app support, and introducing stricter security controls.
Clang
The C/C++/Objective-C compiler used on macOS, part of the LLVM project. Replaced GCC as the default compiler. Invoked via clang or through gcc (which is actually Clang on macOS).
Code Signing
The process of digitally signing executables and apps to verify their identity and integrity. Required for apps distributed through the App Store and increasingly enforced for other software by Gatekeeper.
Coreutils
GNU’s implementation of basic Unix utilities (ls, cp, cat, etc.). Can be installed via Homebrew to get GNU-style behavior, which differs from macOS’s BSD-based versions.
Copy-on-Write (COW)
A resource management technique where copies share the same data until one is modified. APFS uses COW extensively, making file copies and snapshots space-efficient.

D

Daemon
A background process that runs without user interaction, typically started at boot time. On macOS, daemons are managed by launchd and configured in /Library/LaunchDaemons/.
Darwin
The open-source Unix foundation of macOS, iOS, and other Apple operating systems. Darwin includes the XNU kernel, BSD userland components, and various frameworks. Available at opensource.apple.com.
DHCP (Dynamic Host Configuration Protocol)
A network protocol for automatically assigning IP addresses and network configuration to devices.
Disk Image (.dmg)
A file format that contains the contents of a disk or volume. Used for software distribution on macOS. Mounted with hdiutil attach.
Disk Utility
macOS’s GUI tool for managing disks, volumes, and disk images. Command-line equivalent is diskutil.
DNS (Domain Name System)
The system that translates human-readable domain names to IP addresses. Configure on macOS with networksetup -setdnsservers.
DriverKit
Apple’s modern framework for building device drivers that run in user space rather than the kernel, improving system security and stability. Introduced in macOS Catalina.

E

Entitlement
A key-value pair embedded in a code signature that grants specific capabilities to an application (e.g., network access, file access, hardware access). Central to macOS’s security model.
Environment Variable
A named value available to processes, used to configure behavior. Common examples: PATH, HOME, SHELL. Set with export VAR=value.
Extended Attributes
Metadata attached to files beyond standard permissions and timestamps. On macOS, used for Finder info, quarantine flags, and resource forks. View with xattr, list with ls -l@.

F

FileVault
macOS’s full-disk encryption feature using XTS-AES-128 encryption. Encrypts the entire startup volume. Check status with fdesetup status.
Finder
macOS’s default file manager and graphical shell. Access the current directory in Finder with open ..
Firmware
Low-level software stored in hardware that initializes the system before the OS loads. On Apple Silicon Macs, includes iBoot. On Intel Macs, uses EFI.
Fork
In Unix, creating a child process by duplicating the parent. Also refers to resource forks, a legacy macOS method of storing structured data alongside file contents.
Framework
A bundle containing a dynamic shared library along with its headers, resources, and documentation. The macOS equivalent of Linux’s shared libraries with packaging. Located in /System/Library/Frameworks/ and /Library/Frameworks/.
FUSE (Filesystem in Userspace)
A mechanism for implementing filesystems in user space. macFUSE allows mounting alternative filesystems (NTFS, sshfs, etc.) on macOS.

G

Gatekeeper
macOS’s security feature that verifies apps come from identified developers and haven’t been tampered with. Uses code signing and notarization. Check status with spctl --status.
GCD (Grand Central Dispatch)
Apple’s technology for managing concurrent operations through work queues. Available to developers for efficient multi-threading.
GNU
“GNU’s Not Unix” - a project to create a free Unix-like operating system. GNU tools (grep, sed, awk) often have different options than BSD equivalents on macOS.

H

HFS+ (Hierarchical File System Plus)
The legacy filesystem used on macOS before APFS. Still supported for compatibility. Also known as Mac OS Extended.
Homebrew
The most popular package manager for macOS. Installs software to /opt/homebrew/ (Apple Silicon) or /usr/local/ (Intel). Run brew install <package>.

I

Installer Package (.pkg)
A file format for distributing software installers on macOS. Contains installation scripts, payload, and configuration. Install with installer -pkg.
IOKit
The object-oriented framework for device drivers in macOS. Drivers interact with hardware through IOKit APIs. View the hardware registry with ioreg.
IPC (Inter-Process Communication)
Mechanisms for processes to communicate. On macOS, includes Mach ports, Unix signals, pipes, XPC, and distributed notifications.

K

Kernel
The core of an operating system that manages hardware resources and provides services to applications. macOS uses the XNU kernel.
Kernel Extension (kext)
A loadable kernel module on macOS. Being deprecated in favor of System Extensions and DriverKit for security reasons. List with kextstat.
Keychain
macOS’s secure storage for passwords, certificates, and encryption keys. Access from CLI with security command.

L

Launch Agent
A background process managed by launchd that runs in the user session context. Configured in ~/Library/LaunchAgents/ or /Library/LaunchAgents/.
Launch Daemon
A background process managed by launchd that runs at system level (as root or specified user). Configured in /Library/LaunchDaemons/.
launchctl
The command-line interface for interacting with launchd. Use to load, unload, and manage services.
launchd
macOS’s init system and service manager (PID 1). Manages system and user services, replacing traditional Unix init, cron, and other systems.
LLDB
The debugger used on macOS, part of the LLVM project. Replaced GDB. Invoked with lldb.
LLVM
A compiler infrastructure project that includes Clang (C/C++ compiler), LLDB (debugger), and related tools. The foundation of Apple’s development toolchain.

M

Mach
The microkernel that forms the lower layer of XNU. Provides fundamental services like IPC, memory management, and scheduling. Originally developed at Carnegie Mellon University.
Mach-O
The executable file format used on macOS and iOS. Different from Linux’s ELF format. View information with otool or file.
MacPorts
An alternative package manager for macOS (besides Homebrew). Uses /opt/local/ prefix.
man (manual)
The Unix documentation system. View documentation with man <command>.
mDNS (Multicast DNS)
A protocol for resolving hostnames to IP addresses on small networks without a central DNS server. Part of Bonjour.

N

NeXTSTEP
The operating system developed by NeXT (Steve Jobs’s company after leaving Apple). Became the foundation for macOS when Apple acquired NeXT in 1997.
Notarization
Apple’s process of scanning software for malicious content and issues before distribution. Required for software distributed outside the App Store to run without Gatekeeper warnings.
NVRAM (Non-Volatile RAM)
Memory that retains data when powered off. Stores boot-time settings. Access with nvram command.

O

Objective-C
A programming language that adds object-oriented features to C. Historically the primary language for macOS and iOS development, now largely supplanted by Swift.

P

PATH
An environment variable containing a colon-separated list of directories where the shell looks for executable commands.
PID (Process ID)
A unique number identifying a running process. View with ps or Activity Monitor.
Pipe
A mechanism for connecting the output of one command to the input of another. Created with | (e.g., ls | grep foo).
plist (Property List)
An XML or binary format for storing structured data on macOS. Used for preferences, launch agent/daemon configuration, and app settings. Edit with defaults or plutil.
POSIX
Portable Operating System Interface - a family of standards for Unix-like operating systems. macOS is POSIX-compliant, ensuring compatibility with standard Unix tools and APIs.

Q

Quarantine
macOS’s mechanism for marking downloaded files. Triggers Gatekeeper checks when the file is first opened. View with xattr -l (look for com.apple.quarantine).

R

Recovery Mode
A boot environment for troubleshooting and system maintenance. Access by holding power button (Apple Silicon) or Cmd+R (Intel) during startup.
Resource Fork
A legacy macOS feature storing structured data alongside a file’s data fork. Largely replaced by extended attributes but still exists for compatibility.
Root User
The superuser account with full system access. On macOS, disabled by default. Use sudo to execute commands with root privileges.
Rosetta 2
Apple’s translation layer that allows Intel (x86_64) applications to run on Apple Silicon (arm64) Macs.

S

Sandbox
A security mechanism that restricts an application’s access to system resources. App Store apps must be sandboxed. Container data stored in ~/Library/Containers/.
SDK (Software Development Kit)
Tools, libraries, and documentation for developing software. macOS SDKs are included with Xcode or Command Line Tools.
Shell
A command-line interpreter that provides an interface to the operating system. macOS’s default is zsh; bash, tcsh, and others are also available.
Signal
A notification sent to a process. Common signals: SIGTERM (terminate), SIGKILL (force kill), SIGINT (interrupt from Ctrl+C), SIGSTOP (suspend).
SIP (System Integrity Protection)
macOS security feature that restricts root access to protected system files and processes. Check with csrutil status. Can only be modified from Recovery Mode.
Snapshot
A point-in-time copy of a filesystem state. APFS supports efficient snapshots used by Time Machine and system updates.
Spotlight
macOS’s search technology that indexes file contents and metadata. Access from CLI with mdfind.
stderr (Standard Error)
The output stream for error messages, file descriptor 2. Redirect with 2>.
stdin (Standard Input)
The input stream, file descriptor 0. Redirect with <.
stdout (Standard Output)
The primary output stream, file descriptor 1. Redirect with > or >>.
sudo
“Superuser do” - execute a command with elevated privileges. Configuration in /etc/sudoers.
Swift
Apple’s modern programming language for macOS, iOS, and other Apple platforms. Increasingly used for system utilities.
A file that points to another file or directory. Create with ln -s target linkname.
sysctl
Command and interface for viewing and modifying kernel parameters. View with sysctl -a.
System Extension
Modern replacement for kernel extensions that runs in user space. Used for network extensions, endpoint security, and driver extensions.

T

macOS’s privacy protection framework that requires explicit user consent for apps to access sensitive data (contacts, calendar, microphone, screen recording, etc.).
Terminal
An application that provides a text-based interface to the shell. macOS includes Terminal.app; iTerm2 is a popular alternative.
Time Machine
macOS’s backup system using APFS snapshots and incremental backups. CLI tool: tmutil.
TTY
Historically “teletype,” now refers to terminal devices. View current TTY with tty.

U

UID (User ID)
A numeric identifier for a user account. Root is UID 0. View with id.
Unified Logging
macOS’s centralized logging system replacing traditional syslog. Access with log command or Console.app.
Universal Binary
An executable containing code for multiple CPU architectures (e.g., arm64 and x86_64). View architectures with lipo -info or file.
Unix
A family of operating systems originating from AT&T Bell Labs in the 1970s. macOS is a certified Unix operating system.

V

VFS (Virtual File System)
An abstraction layer that provides a common interface for different filesystem implementations.
Volume
A logical storage unit that can be mounted and accessed. On APFS, multiple volumes can share a container.

W

Watchdog
A system that monitors processes and restarts them if they crash. launchd provides watchdog functionality via KeepAlive in plist configurations.

X

x86_64
The 64-bit Intel/AMD processor architecture. Used in Intel Macs (2006-2020).
Xattr
Command for viewing and manipulating extended attributes. Common use: xattr -l file to list attributes.
Xcode
Apple’s integrated development environment (IDE) for macOS, iOS, and other Apple platforms. Includes compilers, debuggers, and Interface Builder.
Xcode Command Line Tools
A standalone package of development tools (compilers, headers, utilities) without the full Xcode IDE. Install with xcode-select --install.
XNU
“X is Not Unix” - the hybrid kernel at the core of macOS. Combines Mach microkernel, BSD components, and IOKit.
XPC (Cross-Process Communication)
Apple’s modern IPC mechanism for communication between processes, particularly for sandboxed apps and system services.
XProtect
macOS’s built-in malware detection and prevention system. Automatically updated by Apple.

Z

zsh (Z Shell)
The default shell on macOS since Catalina. Compatible with bash but adds features like improved tab completion, spelling correction, and plugin support.

Version Names Quick Reference

VersionNameRelease YearNotable Changes
10.0Cheetah2001First Mac OS X release
10.4Tiger2005Spotlight, Dashboard
10.5Leopard2007Time Machine, Spaces
10.6Snow Leopard2009Grand Central Dispatch
10.7Lion2011Full-screen apps, Launchpad
10.9Mavericks2013Free upgrades begin
10.11El Capitan2015System Integrity Protection
10.12Sierra2016Siri, APFS introduced
10.13High Sierra2017APFS default for SSD
10.14Mojave2018Dark mode, privacy controls
10.15Catalina2019zsh default, read-only system
11Big Sur2020Apple Silicon support
12Monterey2021Shortcuts, Focus modes
13Ventura2022Stage Manager
14Sonoma2023Desktop widgets
15Sequoia2024iPhone mirroring

Filesystem Path Quick Reference

PathContents
/Root directory
/Applications/GUI applications
/Library/System-wide resources
/System/macOS system files (protected)
/Users/User home directories
/Volumes/Mounted volumes
/bin/, /sbin/Essential commands
/usr/bin/, /usr/sbin/User commands
/usr/local/Locally installed (Intel Homebrew)
/opt/homebrew/Homebrew (Apple Silicon)
/etc/Configuration (symlink to /private/etc)
/var/Variable data (symlink to /private/var)
/tmp/Temporary files (symlink to /private/tmp)
~/Library/User resources and preferences

Appendix E: Additional Resources

This appendix provides links to documentation, tools, communities, and learning resources for macOS and Unix.


Official Apple Documentation

Developer Documentation

ResourceURLDescription
Apple Developer Documentationdeveloper.apple.com/documentationOfficial API and framework documentation
Mac Technology Overviewdeveloper.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_OverviewSystem architecture overview
Shell Scripting Primerdeveloper.apple.com/library/archive/documentation/OpenSource/Conceptual/ShellScriptingApple’s shell scripting guide
Daemons and Servicesdeveloper.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartuplaunchd and services guide
File System Programming Guidedeveloper.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuideFilesystem concepts and APIs
Security Overviewdeveloper.apple.com/documentation/securitySecurity frameworks and features

System Administration

ResourceURLDescription
Apple Platform Securitysupport.apple.com/guide/securitySecurity architecture guide
macOS Deployment Referencesupport.apple.com/guide/deploymentEnterprise deployment
Mac Admins Documentationsupport.apple.com/guide/mac-helpEnd-user documentation
Apple Configurator Guidesupport.apple.com/guide/apple-configurator-macDevice configuration

Open Source

ResourceURLDescription
Apple Open Sourceopensource.apple.comDarwin and related source code
XNU Sourcegithub.com/apple-oss-distributions/xnuXNU kernel source
Swift Sourcegithub.com/apple/swiftSwift language source

Man Pages and Built-in Documentation

Accessing Man Pages

# View man page
$ man <command>

# Search man pages by keyword
$ man -k <keyword>
$ apropos <keyword>

# View specific section
$ man 5 passwd    # Section 5 (file formats)

# List all sections for a topic
$ man -f passwd
$ whatis passwd

# Convert man page to PDF
$ man -t ls | open -f -a Preview

# Man page sections on macOS
# 1 - User commands
# 2 - System calls
# 3 - C library functions
# 4 - Devices and special files
# 5 - File formats
# 6 - Games
# 7 - Miscellaneous
# 8 - System administration commands

Online Man Pages

ResourceURLDescription
macOS Man Pageskeith.github.io/xcode-man-pagesSearchable macOS man pages
FreeBSD Man Pagesfreebsd.org/cgi/man.cgiBSD reference (often applicable to macOS)
man7.orgman7.org/linux/man-pagesLinux man pages (for comparison)
explainshell.comexplainshell.comVisual command explanation

Package Managers

Homebrew

ResourceURLDescription
Homebrewbrew.shMain site and installation
Homebrew Documentationdocs.brew.shOfficial documentation
Homebrew Formulaeformulae.brew.shPackage search
Homebrew GitHubgithub.com/Homebrew/brewSource code and issues

MacPorts

ResourceURLDescription
MacPortsmacports.orgMain site
MacPorts Guideguide.macports.orgDocumentation
Port Searchports.macports.orgPackage search

Other Package Systems

ResourceURLDescription
Nix on macOSnixos.org/download.htmlNix package manager
pkgsrcpkgsrc.orgNetBSD’s portable package system

Shell Resources

Zsh

ResourceURLDescription
Zsh Manualzsh.sourceforge.io/DocOfficial documentation
Oh My Zshohmyz.shZsh framework and plugins
Preztogithub.com/sorin-ionescu/preztoAlternative zsh framework
Zsh Usersgithub.com/zsh-usersPopular zsh plugins
Awesome Zshgithub.com/unixorn/awesome-zsh-pluginsCurated plugin list

Bash

ResourceURLDescription
Bash Manualgnu.org/software/bash/manualOfficial documentation
Bash Guidemywiki.wooledge.org/BashGuideCommunity guide
Bash Pitfallsmywiki.wooledge.org/BashPitfallsCommon mistakes
ShellCheckshellcheck.netShell script linter

General Shell

ResourceURLDescription
The Art of Command Linegithub.com/jlevy/the-art-of-command-lineCommand line mastery
Command Line Power Usercommandlinepoweruser.comVideo course
tldr pagestldr.shSimplified man pages

Terminal Emulators

ApplicationURLDescription
Terminal.appBuilt-inmacOS default terminal
iTerm2iterm2.comFeature-rich terminal
Alacrittyalacritty.orgGPU-accelerated terminal
kittysw.kovidgoyal.net/kittyFast, feature-rich terminal
Warpwarp.devModern terminal with AI
Hyperhyper.isElectron-based terminal
Tabbytabby.shCross-platform terminal

Text Editors

Terminal-Based

EditorURLDescription
Vimvim.orgClassic modal editor
Neovimneovim.ioModern Vim fork
GNU Emacsgnu.org/software/emacsExtensible editor
nanoBuilt-inSimple editor
micromicro-editor.github.ioModern terminal editor

GUI with Terminal Integration

EditorURLDescription
Visual Studio Codecode.visualstudio.comPopular extensible editor
Sublime Textsublimetext.comFast, powerful editor
BBEditbarebones.com/products/bbeditmacOS-native text editor

Development Resources

Command Line Tools

ResourceURLDescription
Xcode Downloadsdeveloper.apple.com/downloadXcode and tools
Xcode Release Notesdeveloper.apple.com/documentation/xcode-release-notesVersion history

Version Control

ResourceURLDescription
Pro Git Bookgit-scm.com/bookComprehensive Git book
GitHub CLIcli.github.comGitHub command line
Git Documentationgit-scm.com/docOfficial documentation

Language-Specific

ResourceURLDescription
pyenvgithub.com/pyenv/pyenvPython version management
rbenvgithub.com/rbenv/rbenvRuby version management
nvmgithub.com/nvm-sh/nvmNode.js version management
rustuprustup.rsRust toolchain installer

System Administration

Mac Admin Resources

ResourceURLDescription
Mac Admins Foundationmacadmins.orgCommunity resources
MacAdmins Slackmacadmins.slack.comCommunity chat
Der Flounderderflounder.wordpress.comMac admin blog
Mr. Macintoshmrmacintosh.commacOS news and guides
Scripting OS Xscriptingosx.comAutomation and scripting
Mac Admin Infomacadmin.infoTools and resources

Configuration Management

ToolURLDescription
Munkigithub.com/munki/munkiSoftware deployment
Jamfjamf.comEnterprise management
Mosylemosyle.comApple device management
Puppetpuppet.comConfiguration management
Ansibleansible.comAutomation platform
Chefchef.ioInfrastructure automation

Security Tools

ToolURLDescription
Objective-See Toolsobjective-see.org/tools.htmlFree security tools
Santagithub.com/google/santaApplication allowlisting
osqueryosquery.ioSystem information via SQL
Luluobjective-see.org/products/lulu.htmlOpen-source firewall

Books

macOS and Unix

TitleAuthorDescription
macOS InternalsJonathan LevinDeep dive into macOS architecture
The Mac Hacker’s HandbookMiller & Dai ZovimacOS security
Learning Unix for OS XDave TaylorIntroduction for Mac users
Mac OS X for Unix GeeksJepson & RothmanUnix perspective on macOS

Unix and Linux

TitleAuthorDescription
The Linux Command LineWilliam ShottsFree online
Unix and Linux System Administration HandbookNemeth et al.Comprehensive sysadmin
How Linux WorksBrian WardUnderstanding Linux internals
The Unix Programming EnvironmentKernighan & PikeClassic Unix philosophy
Advanced Programming in the Unix EnvironmentStevens & RagoUnix programming bible

Shell Scripting

TitleAuthorDescription
Learning the bash ShellNewham & RosenblattBash fundamentals
Classic Shell ScriptingRobbins & BeebePOSIX shell scripting
Wicked Cool Shell ScriptsTaylor & PerryPractical scripts
From Bash to Z ShellKiddle et al.Advanced shell usage

Community Resources

Forums and Q&A

ResourceURLDescription
Stack Overflowstackoverflow.com/questions/tagged/macosProgramming Q&A
Ask Differentapple.stackexchange.comApple-focused Q&A
Unix & Linux Stack Exchangeunix.stackexchange.comUnix Q&A
Super Usersuperuser.comPower user Q&A

Discussion

ResourceURLDescription
MacRumors Forumsforums.macrumors.comMac community
r/MacOSreddit.com/r/MacOSmacOS subreddit
r/commandlinereddit.com/r/commandlineCLI subreddit
r/osxreddit.com/r/osxLegacy macOS subreddit
Hacker Newsnews.ycombinator.comTech news and discussion

Conferences and Events

EventURLDescription
WWDCdeveloper.apple.com/wwdcApple developer conference
MacDevOps:YVRmacdevops.caMac admin conference
MacSysAdminmacsysadmin.seEuropean Mac admin conference
Objective by the Seaobjectivebythesea.orgmacOS security conference

Blogs and News

Technical Blogs

ResourceURLDescription
Eclectic Lighteclecticlight.comacOS technical deep-dives
The Eclectic Light Companyeclecticlight.co/tag/macs/Howard Oakley’s blog
Scripting OS Xscriptingosx.comArmin Briegel’s blog
Der Flounderderflounder.wordpress.comRich Trouton’s blog
Sixcolorssixcolors.comJason Snell’s Apple coverage

News Sites

ResourceURLDescription
MacRumorsmacrumors.comApple news
9to5Mac9to5mac.comApple news
Ars Technicaarstechnica.com/appleIn-depth Apple coverage
AppleInsiderappleinsider.comApple news and reviews

Useful Utilities

System Utilities

ToolURLDescription
htopbrew install htopInteractive process viewer
ncdubrew install ncduNCurses disk usage
treebrew install treeDirectory tree listing
jqbrew install jqJSON processor
ripgrepbrew install ripgrepFast search tool
fdbrew install fdFast find alternative
batbrew install batCat with syntax highlighting
exa/ezabrew install ezaModern ls replacement
fzfbrew install fzfFuzzy finder
tmuxbrew install tmuxTerminal multiplexer

macOS-Specific

ToolURLDescription
masbrew install masMac App Store CLI
dutibrew install dutiSet default applications
m-clibrew install m-climacOS CLI swiss army knife
mackupbrew install mackupApplication settings backup
trashbrew install trashMove files to Trash
terminal-notifierbrew install terminal-notifierSend notifications
blueutilbrew install blueutilBluetooth CLI
switchaudio-osxbrew install switchaudio-osxAudio device switching

Learning Paths

Beginner

  1. Read Apple’s Shell Scripting Primer
  2. Work through The Linux Command Line (free online)
  3. Practice with tldr pages and explainshell
  4. Install Homebrew and explore packages
  5. Learn basic vim or nano for quick edits

Intermediate

  1. Master zsh configuration and plugins
  2. Learn shell scripting and automation
  3. Understand launchd for services
  4. Explore system administration tools
  5. Study filesystem hierarchy and permissions

Advanced

  1. Read macOS Internals by Jonathan Levin
  2. Explore XNU source code
  3. Study security architecture (Gatekeeper, SIP, TCC)
  4. Learn IOKit and system frameworks
  5. Contribute to open-source macOS tools

Essential Bookmarks

Apple Developer          https://developer.apple.com
Homebrew                 https://brew.sh
iTerm2                   https://iterm2.com
Oh My Zsh                https://ohmyz.sh
explainshell             https://explainshell.com
tldr pages               https://tldr.sh
ShellCheck               https://shellcheck.net
Mac Admin Slack          https://macadmins.herokuapp.com

Documentation Quick Access

macOS Man Pages          https://keith.github.io/xcode-man-pages/
Homebrew Docs            https://docs.brew.sh
Zsh Manual               https://zsh.sourceforge.io/Doc/
Apple Open Source        https://opensource.apple.com
Apple Platform Security  https://support.apple.com/guide/security/