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:
~/.zshrcexpands 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:
- The XNU kernel: A hybrid design merging the Mach microkernel with BSD components
- A BSD userland: Command-line tools and libraries derived primarily from FreeBSD
- 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:
| Component | License |
|---|---|
| XNU kernel | Apple Public Source License (APSL) 2.0 |
| BSD userland | Various BSD licenses |
| IOKit | APSL 2.0 |
| Mach components | Various (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:
| Darwin | macOS |
|---|---|
| Open-source Unix foundation | Complete commercial product |
| Kernel + userland tools | Darwin + Aqua GUI + Apple frameworks + apps |
| No graphical interface | Full desktop experience |
| Freely redistributable | Licensed 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 Version | Marketing Name | Darwin Version |
|---|---|---|
| macOS 14.x | Sonoma | 23.x |
| macOS 13.x | Ventura | 22.x |
| macOS 12.x | Monterey | 21.x |
| macOS 11.x | Big Sur | 20.x |
| macOS 10.15 | Catalina | 19.x |
| macOS 10.14 | Mojave | 18.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:
- Certified Unix: Darwin-based macOS is UNIX 03 certified
- BSD heritage: Derived primarily from FreeBSD
- Unique kernel: XNU is unlike both monolithic Linux and pure Mach microkernels
- Apple stewardship: Developed primarily by Apple, with limited community input
- 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:
- NeXTSTEP technology: The foundation for the next-generation Mac OS
- Steve Jobs: Who would eventually return as CEO
- 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:
| Year | Event |
|---|---|
| 1985 | Steve Jobs leaves Apple, founds NeXT |
| 1988 | NeXTSTEP 0.8 released |
| 1989 | NeXTSTEP 1.0 released |
| 1993 | NeXTSTEP 3.0 - mature, stable release |
| 1994 | OPENSTEP specification published |
| 1996 | Apple acquires NeXT |
| 1997 | Steve Jobs returns to Apple |
| 1999 | Mac OS X Server 1.0 released |
| 2000 | Darwin open-sourced |
| 2001 | Mac OS X 10.0 “Cheetah” released |
| 2012 | OS X 10.8 “Mountain Lion” - “Mac” dropped from name |
| 2016 | macOS 10.12 “Sierra” - rebranded to “macOS” |
| 2020 | macOS 11 “Big Sur” - first version for Apple Silicon |
Why This History Matters
Understanding NeXTSTEP’s influence helps explain macOS peculiarities:
- Why Objective-C? Because NeXTSTEP chose it in the 1980s
- Why property lists? NeXTSTEP’s serialization format
- Why application bundles? NeXTSTEP’s packaging model
- Why Mach ports? NeXTSTEP’s kernel choice
- 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:
- Why Mach-O binaries? XNU uses Mach-O format, not ELF like Linux
- Why different process tools? BSD and Mach layers provide overlapping but different views
- Why complex memory semantics? Mach’s VM model has unique characteristics
- 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
| System | Kernel | Type |
|---|---|---|
| macOS | XNU | Hybrid (Mach + BSD) |
| Linux | Linux | Monolithic (with modules) |
| FreeBSD | FreeBSD | Monolithic (with modules) |
| OpenBSD | OpenBSD | Monolithic |
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
| System | Init System | Config Format |
|---|---|---|
| macOS | launchd | Property lists (XML/binary) |
| Linux (modern) | systemd | Unit files (INI-like) |
| Linux (traditional) | SysVinit | Shell scripts |
| FreeBSD | rc.d | Shell 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
readlink
# 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
| System | Default FS | Features |
|---|---|---|
| macOS | APFS | Snapshots, encryption, space sharing, clones |
| Linux | ext4 / XFS / Btrfs | Varies by filesystem |
| FreeBSD | ZFS / UFS | ZFS 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):
| Purpose | Linux/BSD FHS | macOS |
|---|---|---|
| Applications | /usr/bin, /opt | /Applications |
| System config | /etc | /etc (Unix) + /Library/Preferences (macOS) |
| User config | ~/.config | ~/Library |
| User data | Various | ~/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:
| System | Native Package Manager |
|---|---|
| macOS | None (uses App Store for GUI apps) |
| Debian/Ubuntu | apt |
| Red Hat/Fedora | dnf/yum |
| Arch | pacman |
| FreeBSD | pkg |
| OpenBSD | pkg_add |
Solutions for macOS:
# Homebrew (most popular)
$ brew install wget
# MacPorts (alternative)
$ sudo port install wget
User and Permission Differences
User IDs
| User | Linux | macOS |
|---|---|---|
| root | UID 0 | UID 0 |
| First user | UID 1000 | UID 501 |
| Nobody | UID 65534 | UID -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
| Feature | macOS | Linux | FreeBSD |
|---|---|---|---|
| Kernel | XNU (hybrid) | Linux (monolithic) | FreeBSD (monolithic) |
| Init | launchd | systemd (usually) | rc.d |
| Package mgr | Homebrew (3rd party) | apt/dnf/pacman | pkg |
| Shell default | zsh | bash (usually) | sh/tcsh |
| Filesystem | APFS | ext4/XFS/Btrfs | ZFS/UFS |
| Case sensitive | No (default) | Yes | Configurable |
| Binary format | Mach-O | ELF | ELF |
| Commands | BSD | GNU | BSD |
| Firewall | pf | iptables/nftables | pf/ipfw |
| Root restriction | SIP | SELinux/AppArmor | securelevel |
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:
- Passing conformance tests
- Paying certification fees
- 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
Common POSIX-Related Issues
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
- Use
#!/bin/shand stick to POSIX features - Test with
shellcheck --shell=sh - Avoid command options not listed in POSIX
- Use
printfinstead ofechofor complex output - 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
- Include
_POSIX_C_SOURCEor_XOPEN_SOURCEappropriately - Check return values (POSIX mandates specific error codes)
- Use
configurescripts to detect platform differences - 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
- POSIX.1-2017 specification
- Apple’s UNIX Conformance documentation
- IEEE Std 1003.1 (requires IEEE subscription)
Summary
macOS’s POSIX compliance and UNIX certification mean:
- Standard APIs work: POSIX C functions behave correctly
- Basic commands exist: Core utilities are available
- Shell scripts can be portable: With discipline
- 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
shellcheckandautoconf - 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:
- APFS: The modern storage technology providing advanced features like snapshots, clones, and space sharing
- The VFS layer: How macOS presents a unified filesystem interface to applications
- 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
/optexists or/tmppersists 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:
| Role | Purpose | Mount Point |
|---|---|---|
| System | Boot volume, read-only | / |
| Data | User data | /System/Volumes/Data (firmlinked to appear at /Users, /Library, etc.) |
| VM | Swap and sleep image | /System/Volumes/VM |
| Preboot | Boot helper data | /System/Volumes/Preboot |
| Recovery | Recovery OS | Not 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:
- Write new content to a temporary file
- Atomically rename temporary file to replace original
- 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
| Feature | APFS | HFS+ | ext4 | ZFS | Btrfs |
|---|---|---|---|---|---|
| Copy-on-Write | Partial | No | No | Yes | Yes |
| Snapshots | Yes | No | No | Yes | Yes |
| Clones | Yes | No | No | Yes | Yes |
| Space Sharing | Yes | No | No | Yes | Yes |
| Data Checksums | Metadata only | No | Metadata | Yes | Yes |
| Compression | No | Limited | No | Yes | Yes |
| Encryption | Native | Via Core Storage | Via LUKS | Yes | Via dm-crypt |
| Flash Optimized | Yes | No | Partial | Yes | Yes |
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:
| Variant | Description |
|---|---|
| Mac OS Extended | Base 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
| Limit | HFS+ |
|---|---|
| Maximum volume size | 8 EB (theoretical) |
| Maximum file size | 8 EB (theoretical) |
| Maximum files | ~4.3 billion |
| Maximum filename | 255 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:
.TXTshould equal.txt - Application bundles:
Safari.appandsafari.appshouldn’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:
- Establish naming conventions: Always use lowercase, or establish explicit conventions
- 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
- 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
| Attribute | Purpose |
|---|---|
com.apple.quarantine | Marks files downloaded from internet |
com.apple.FinderInfo | Finder metadata (color labels, etc.) |
com.apple.ResourceFork | Legacy resource fork data |
com.apple.metadata:* | Spotlight metadata |
com.apple.lastuseddate#PS | Last used timestamp |
com.apple.macl | macOS Access Control List |
com.apple.provenance | File 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
| Tool | Preserves xattrs | Notes |
|---|---|---|
cp | Yes (with -p) | macOS cp includes -p in default behavior |
mv | Yes | Within same filesystem |
rsync -X | Yes | Requires -X flag |
tar --xattrs | Yes | Requires flag |
ditto | Yes | Recommended for Mac-to-Mac |
asr | Yes | Apple Software Restore |
Tools That Don’t Preserve Attributes
| Tool | Issue | Workaround |
|---|---|---|
scp | Drops attributes | Use rsync -avzX |
git | Not version controlled | Document requirements |
curl/wget | Adds quarantine | Remove 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:
- Be aware that attributes exist and affect behavior
- Use appropriate tools (
ditto,rsync -X) when preservation matters - Know how to inspect (
xattr -l) and remove (xattr -d) attributes - Understand AppleDouble files when working with non-Mac filesystems
- 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.
Globally (Not Recommended)
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:
- Clean before sharing:
find /path/to/share -name '.DS_Store' -delete
find /path/to/share -name '._*' -delete
- Use appropriate tools:
# For cross-platform ZIP
zip -r archive.zip folder/ -x "*.DS_Store" -x "._*"
- 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 Type | Purpose | When to Remove |
|---|---|---|
.DS_Store | Finder view settings | Version control, cross-platform sharing |
._* files | Extended attributes on non-Mac filesystems | When no longer needed, cross-platform sharing |
__MACOSX/ | Resource forks in ZIP files | Cross-platform distribution |
Key commands:
find . -name '.DS_Store' -delete— Clean.DS_Storefilesfind . -name '._*' -delete— Clean AppleDouble filesdot_clean /path— Merge AppleDouble files backdefaults 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)
| Purpose | FHS (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,/tmpwork as expected/usr/localis available for local software
macOS-specific:
/Applicationsfor GUI applications/Libraryhierarchy for system/user resources/Systemprotected by SIP/Volumesfor all mounted volumes/Usersinstead of/home/privatecontaining the actual/etc,/var,/tmp
Key differences from Linux:
- Configuration often in property lists, not
/etctext files - Three-tier Library structure (
/System/Library,/Library,~/Library) - Homebrew in
/opt/homebrewon 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:
| Task | Command |
|---|---|
| List disks | diskutil list |
| Disk info | diskutil info disk0 |
| Mount volume | diskutil mount disk3s5 |
| Unmount volume | diskutil unmount disk3s5 |
| Eject disk | diskutil eject disk2 |
| Format disk | diskutil eraseDisk APFS "Name" GPT disk2 |
| Format volume | diskutil eraseVolume APFS "Name" disk2s2 |
| Add APFS volume | diskutil apfs addVolume disk3 APFS "Name" |
| Create disk image | hdiutil create -size 1g -fs APFS name.dmg |
| Mount disk image | hdiutil attach image.dmg |
| Verify filesystem | diskutil verifyVolume disk3s5 |
| Repair filesystem | diskutil 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 Partition | APFS Container |
|---|---|
| Fixed size | Fixed size |
| One filesystem | Multiple volumes |
| Resizing requires unmount | Volumes resize dynamically |
| Independent of other partitions | Volumes 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:
| Role | Purpose | Characteristics |
|---|---|---|
| System | Boot OS | Read-only, sealed |
| Data | User data | Read-write |
| VM | Swap space | Not persistent |
| Preboot | Boot helpers | Small, specific files |
| Recovery | Recovery OS | Hidden, 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:
| Concept | Description | Key Commands |
|---|---|---|
| Container | Pool of storage | diskutil apfs list, createContainer, deleteContainer |
| Volume | Filesystem within container | addVolume, deleteVolume, setQuota |
| Snapshot | Point-in-time capture | listSnapshots, deleteSnapshot, tmutil |
| Role | Volume 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:
- Open Terminal: Press
Cmd+Space, type “Terminal”, press Enter - You’re now in zsh: The default shell since Catalina
- Your home directory: Terminal starts in
~(your home folder) - 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:
| Bash | Zsh | Purpose |
|---|---|---|
~/.bash_profile | ~/.zprofile | Login shell setup |
~/.bashrc | ~/.zshrc | Interactive shell config |
~/.bash_login | ~/.zlogin | Login shell (after profile) |
~/.bash_logout | ~/.zlogout | Logout cleanup |
| - | ~/.zshenv | All 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:
| Aspect | Recommendation |
|---|---|
| New users | Use zsh (default), it’s excellent |
| Existing bash users | Migrate gradually, zsh is compatible with most setups |
| Scripts | Specify interpreter explicitly (#!/bin/bash or #!/bin/zsh) |
| Portability | Use #!/bin/sh for maximum compatibility |
| Bash 4+ features needed | Install 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
- Click
+to add a new profile - Name it (e.g., “Development”)
- Configure settings
- 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
Recommended Settings
For a good development experience:
- Font: SF Mono or Menlo, 13pt
- Window size: 120×35 or larger
- Terminal type:
xterm-256color - Option as Meta: Enable (for Alt key bindings)
- Scroll: Enable “Scroll to bottom on input”
Working with Windows and Tabs
Keyboard Shortcuts
| Action | Shortcut |
|---|---|
| New window | Cmd+N |
| New tab | Cmd+T |
| Close tab/window | Cmd+W |
| Next tab | Cmd+Shift+] or Ctrl+Tab |
| Previous tab | Cmd+Shift+[ or Ctrl+Shift+Tab |
| Specific tab | Cmd+1 through Cmd+9 |
| Split pane (macOS Catalina+) | Not built-in (use iTerm2) |
Window Groups
Save and restore window arrangements:
- Arrange windows as desired
- Window → Save Windows as Group
- Name the group
- Restore: Window → Open Window Group
Tab Bar
Show tab bar even with single tab:
- View → Show Tab Bar
- Or:
Cmd+Shift+Ttoggles tab bar visibility
Text Selection and Editing
Selection Shortcuts
| Action | Method |
|---|---|
| Select word | Double-click |
| Select line | Triple-click |
| Select URL | Cmd+Double-click |
| Rectangular selection | Hold Option, drag |
| Extend selection | Shift+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
| Action | Shortcut |
|---|---|
| Jump to previous mark | Cmd+Up |
| Jump to next mark | Cmd+Down |
| Select between marks | Cmd+Shift+Up/Down |
| Bookmark current line | Cmd+U |
| Jump to bookmark | Cmd+Option+U |
Searching
| Action | Shortcut |
|---|---|
| Find | Cmd+F |
| Find next | Cmd+G |
| Find previous | Cmd+Shift+G |
| Use selection for find | Cmd+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:
- Terminal → Preferences → Profiles
- Click gear icon → Import
- Select
.terminalfile
Popular theme sources:
Creating Custom Colors
- Preferences → Profiles → Text
- Click color swatches to customize
- 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:
- Edit → Notify When Running Process Completes
- 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:
- Terminal → Secure Keyboard Entry (or
Cmd+Shift+S) - Prevents other applications from reading keystrokes
- 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:
- Make selection (optional—prints all if nothing selected)
- File → Print or
Cmd+P - Options: Include timestamp, color, etc.
Accessibility
VoiceOver Support
Terminal works with VoiceOver (screen reader):
Cmd+F5to 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+Clickto 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:
| Feature | Available |
|---|---|
| 256 colors | Yes |
| Unicode/UTF-8 | Yes |
| Tabs | Yes |
| Split panes | No (use iTerm2) |
| Shell integration | Yes |
| Profiles | Yes |
| Touch Bar | Yes |
| Secure input | Yes |
| AppleScript | Yes |
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:
| Action | Shortcut |
|---|---|
| Split vertically | Cmd+D |
| Split horizontally | Cmd+Shift+D |
| Navigate panes | Cmd+Option+Arrow |
| Maximize pane | Cmd+Shift+Enter |
| Close pane | Cmd+W |
Hotkey Window
A terminal that slides down from the top (like Quake consoles):
- Preferences → Keys → Hotkey
- Create a Dedicated Hotkey Window
- Assign a hotkey (e.g.,
Option+Space) - 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:
- Preferences → Profiles → Advanced → Triggers
- Add regex patterns
- 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:
- Press
Cmd+Option+B - Use arrow keys to move through history
- Press Escape to return to live view
Password Manager
Store frequently-used passwords:
- Preferences → Profiles → Advanced → Triggers
- Or: Window → Password Manager (
Cmd+Option+F) - Store username/password pairs
- 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:
- Shell → Broadcast Input
- Options: To All Panes, To All Tabs, etc.
- 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:
- Preferences → Profiles → Keys
- Key Mappings → Presets → Natural Text Editing
This enables:
Option+Left/Right- Move by wordOption+Backspace- Delete wordCmd+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)
Recommended Settings
For productivity:
- Enable hotkey window (
Option+Space) - Install shell integration
- Enable Natural Text Editing
- Configure triggers for errors/completions
- 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
| Feature | Terminal.app | iTerm2 | Alacritty | Kitty | Warp |
|---|---|---|---|---|---|
| Split panes | No | Yes | No | Yes | Yes |
| GPU rendering | No | Optional | Yes | Yes | Yes |
| Tabs | Yes | Yes | No | Yes | Yes |
| Shell integration | Basic | Advanced | No | Yes | Advanced |
| Profiles | Yes | Yes | No | Yes | Yes |
| Images inline | No | Yes | Yes | Yes | Yes |
| Hotkey window | No | Yes | No | No | No |
| Cross-platform | No | No | Yes | Yes | No |
| Configuration | GUI | GUI | TOML | conf | GUI |
| Price | Free | Free | Free | Free | Freemium |
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:
- Start with Terminal.app - Learn the basics
- Graduate to iTerm2 - Most macOS power users land here
- 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
/etc/zshenv- System-wide, all shells~/.zshenv- User, all shells (including scripts)
Login Shells
/etc/zprofile- System-wide login~/.zprofile- User login
Interactive Shells
/etc/zshrc- System-wide interactive~/.zshrc- User interactive
Login Shell Completion
/etc/zlogin- System-wide, after zshrc~/.zlogin- User, after zshrc
Logout
~/.zlogout- User logout cleanup/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)
Recommended File Usage
| File | Use For |
|---|---|
~/.zshenv | Environment variables needed by scripts (rarely modified) |
~/.zprofile | Login-specific setup (PATH modifications for login shells) |
~/.zshrc | Interactive configuration (aliases, prompts, completion, key bindings) |
~/.zlogin | Commands that should run after zshrc (rarely used) |
~/.zlogout | Cleanup on logout (rarely used) |
Bash Configuration Files
For those still using bash:
Login Shells
/etc/profile- System-wide- First found of:
~/.bash_profile,~/.bash_login,~/.profile
Non-Login Interactive Shells
/etc/bash.bashrc(not on macOS by default)~/.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:
compinitwithout 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:
| Shell | Main Config | Login Config |
|---|---|---|
| zsh | ~/.zshrc | ~/.zprofile |
| bash | ~/.bashrc | ~/.bash_profile |
Best practices:
- Keep
.zshenvminimal — runs for all shells - Put PATH in
.zprofile— for login shells - Put interactive config in
.zshrc— aliases, prompts, completion - Use
.zshrc.local— for machine-specific settings - Version control your dotfiles — but not secrets
- 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
| Variable | Purpose | Example |
|---|---|---|
PATH | Executable search path | /usr/bin:/bin |
HOME | User home directory | /Users/david |
USER | Current username | david |
SHELL | Default shell | /bin/zsh |
TERM | Terminal type | xterm-256color |
LANG | Locale setting | en_US.UTF-8 |
EDITOR | Default text editor | vim |
TMPDIR | Temporary directory | /var/folders/.../T/ |
PWD | Current working directory | /Users/david/project |
macOS-Specific Variables
| Variable | Purpose |
|---|---|
TERM_PROGRAM | Terminal app name (Apple_Terminal, iTerm.app) |
TERM_SESSION_ID | Unique session identifier |
__CFBundleIdentifier | Current app bundle ID |
COMMAND_MODE | Unix compatibility mode |
SECURITYSESSIONID | Security 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:
- System-wide PATH (
/etc/paths):
$ cat /etc/paths
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin
- PATH additions (
/etc/paths.d/*):
$ ls /etc/paths.d
100-rvictl
MacGPG2
$ cat /etc/paths.d/MacGPG2
/usr/local/MacGPG2/bin
- Shell configuration (
~/.zprofile,~/.zshrc):
export PATH="/opt/homebrew/bin:$PATH"
- 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.osxin 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:
| Context | Config Location | Inheritance |
|---|---|---|
| Login shell | ~/.zprofile | From system |
| Interactive shell | ~/.zshrc | From login shell |
| Scripts | Minimal | Must be explicit |
| GUI apps | None | launchctl setenv |
| SSH | ~/.ssh/environment | Explicit |
| Subprocesses | - | From parent |
Key points:
- PATH order matters — first match wins
- GUI apps don’t get shell environment — use launchctl or launch from terminal
- Homebrew needs explicit PATH setup — add in
.zprofile - Scripts have minimal environment — use full paths or set PATH explicitly
- 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:
| Command | Purpose |
|---|---|
pbcopy/pbpaste | Clipboard access |
open | Open files/URLs/apps |
mdfind | Spotlight search |
mdls | View file metadata |
say | Text-to-speech |
screencapture | Screenshots/recordings |
osascript | Run AppleScript |
sw_vers | macOS version |
system_profiler | System information |
pmset | Power management |
networksetup | Network configuration |
diskutil | Disk 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
- Open Automator
- Create new “Quick Action”
- Add “Run Shell Script” action
- Write your script
- 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:
| Service | Command/Method |
|---|---|
| Finder | open, osascript |
| Keychain | security |
| AppleScript | osascript |
| Notifications | osascript, terminal-notifier |
| Quick Look | qlmanage |
| Spotlight | mdfind, mdls |
| Shortcuts | shortcuts |
| Calendar | osascript |
| Reminders | osascript |
Key integration patterns:
- Use
osascriptfor GUI application control - Use
securityfor credential management - Use
terminal-notifierfor user feedback - 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 installworkflow
MacPorts
An older, more Unix-traditional approach:
- Builds everything from source by default
- Extensive package collection
- More isolation from system
- Uses
/opt/localprefix
Native Installers
Apple’s standard installation methods:
.pkgfiles for command-line tools.dmgdisk images for applications- Xcode Command Line Tools
- App Store for sandboxed applications
Language-Specific Managers
Development often requires language-specific tools:
pip/pyenvfor Pythongem/rbenv/rvmfor Rubynpm/nvmfor Node.jscargofor Rustgo getfor 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 (
/optfor 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 descriptionhomepage: Project websiteurl: Source code download locationsha256: Checksum for verificationdepends_on: Dependenciesinstall: Build and installation commandstest: 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
Symlinks and opt
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/opensslalways 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 repositoryhomebrew/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:
| Component | Purpose | Location |
|---|---|---|
| Formula | Package definition | Taps (Git repos) |
| Cellar | Installed packages | /opt/homebrew/Cellar/ |
| Bottles | Pre-built binaries | GitHub Container Registry |
| Taps | Formula repositories | ~/.../Taps/ |
| Casks | GUI app installers | homebrew/cask tap |
| opt | Stable 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:
| Practice | Command |
|---|---|
| Update regularly | brew update && brew upgrade |
| Clean up | brew cleanup |
| Check health | brew doctor |
| Remove orphans | brew autoremove |
| Pin critical packages | brew pin package |
| Use Brewfile | brew bundle dump |
| Audit before adding taps | Research tap source |
Golden rules:
- Update before installing —
brew updatefirst - Use bottles when possible — Much faster than source
- Run cleanup regularly — Saves disk space
- Use Brewfile for teams — Reproducible environments
- Don’t sudo brew — Never run Homebrew with sudo
- 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:
- Self-contained: Installs entirely under
/opt/local, separate from system - Build from source: Compiles everything by default
- Explicit variants: Fine-grained control over build options
- 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
| Aspect | MacPorts | Homebrew |
|---|---|---|
| Installation prefix | /opt/local | /opt/homebrew or /usr/local |
| Default build | From source | Pre-built bottles |
| System integration | Self-contained | Uses macOS libraries |
| Sudo required | Yes | No |
| Build customization | Variants | Limited options |
| Package count | ~27,000 | ~6,000 formulae |
| Speed | Slower (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:
| Action | Command |
|---|---|
| Search | port search name |
| Info | port info name |
| Install | sudo port install name |
| Update | sudo port selfupdate |
| Upgrade | sudo port upgrade outdated |
| Uninstall | sudo port uninstall name |
| Clean | sudo 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
| Format | Description | Management |
|---|---|---|
.pkg | Installer package | pkgutil, installer |
.dmg | Disk image containing .app | hdiutil, manual |
.app | Application bundle | Manual drag to /Applications |
.mpkg | Meta-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:
| Method | Use Case | Management |
|---|---|---|
.pkg | System tools, CLT | pkgutil, installer |
.dmg | App distribution | hdiutil |
.app | Direct app bundle | cp, rm |
| App Store | Sandboxed apps | mas |
| softwareupdate | macOS/system | softwareupdate |
Key commands:
| Task | Command |
|---|---|
| Install pkg | sudo installer -pkg file.pkg -target / |
| List packages | pkgutil --pkgs |
| Package files | pkgutil --files com.example.pkg |
| Forget package | sudo pkgutil --forget com.example.pkg |
| Mount dmg | hdiutil attach file.dmg |
| Unmount dmg | hdiutil detach /Volumes/Name |
| System updates | softwareupdate --list |
| Install CLT | xcode-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?
| Source | Recommendation |
|---|---|
| System Python | Never — don’t modify |
| Xcode CLT Python | For basic scripting only |
| Homebrew | Good for single-version needs |
| pyenv | Recommended — version management |
| python.org | Alternative 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
Recommended Setup
For Most Developers
- Install pyenv:
$ brew install pyenv pyenv-virtualenv
# Add shell configuration
- Install Python versions:
$ pyenv install 3.11.7
$ pyenv install 3.12.0
$ pyenv global 3.11.7
- 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:
| Scenario | Recommendation |
|---|---|
| Single Python version | Homebrew python@3.x |
| Multiple versions | pyenv |
| Project isolation | venv or pyenv-virtualenv |
| Dependency management | Poetry or pip + requirements.txt |
| Data science | Conda/Miniconda |
Key commands:
| Task | Command |
|---|---|
| Install Python (Homebrew) | brew install python@3.11 |
| Install Python (pyenv) | pyenv install 3.11.7 |
| Set global version | pyenv global 3.11.7 |
| Set project version | pyenv local 3.11.7 |
| Create virtualenv | python3 -m venv env |
| Activate virtualenv | source env/bin/activate |
| Install packages | pip install package |
Golden rules:
- Never modify system Python
- Always use virtual environments
- Pick one version manager (pyenv recommended)
- 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: Recommended Approach
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: Full-Featured Alternative
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
| Feature | rbenv | rvm |
|---|---|---|
| Philosophy | Minimal | Full-featured |
| Gemsets | No (use bundler) | Yes |
| Shell modification | Shims | PATH modification |
| Complexity | Low | Higher |
| Recommendation | Preferred | If 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:
| Tool | Best For |
|---|---|
| rbenv | Most users (recommended) |
| chruby | Minimalists |
| rvm | Users needing gemsets |
| Homebrew ruby | Single version, casual use |
Key commands (rbenv):
| Task | Command |
|---|---|
| Install rbenv | brew install rbenv ruby-build |
| Install Ruby | rbenv install 3.2.2 |
| Set global | rbenv global 3.2.2 |
| Set local | rbenv local 3.2.2 |
| List versions | rbenv versions |
| Install gem | gem install name |
| Project gems | bundle install |
Best practices:
- Never use system Ruby
- Use rbenv for version management
- Use Bundler for project dependencies
- Commit Gemfile.lock to version control
- 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
| Method | Pros | Cons |
|---|---|---|
| nvm | Most popular, well-supported | Shell startup overhead |
| fnm | Fast, Rust-based | Newer, less documentation |
| volta | Also manages npm/yarn | Newer tool |
| Homebrew | Simple | Single version only |
| Official installer | Direct from source | Manual 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
| Feature | fnm | nvm |
|---|---|---|
| Speed | Fast (Rust) | Slower (shell) |
| .nvmrc support | Yes | Yes |
| Completions | Yes | Yes |
| Windows support | Yes | No (use nvm-windows) |
| Maturity | Newer | Established |
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
| Manager | Best For |
|---|---|
| npm | Default, wide compatibility |
| yarn | Workspaces, performance |
| pnpm | Disk 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:
| Tool | Recommendation |
|---|---|
| Version manager | fnm (fast) or nvm (established) |
| Package manager | npm (default), yarn/pnpm for specific needs |
| Project config | .nvmrc + engines in package.json |
Key commands (nvm):
| Task | Command |
|---|---|
| Install nvm | curl -o- https://raw.githubusercontent.com/.../nvm/.../install.sh | bash |
| Install Node | nvm install 20 |
| Use version | nvm use 20 |
| Set default | nvm alias default 20 |
| Project version | echo "20" > .nvmrc |
| List versions | nvm ls |
Best practices:
- Use a version manager (nvm or fnm)
- Include .nvmrc in projects
- Set engines in package.json
- Don’t sudo npm install -g
- 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
| Aspect | Homebrew | MacPorts |
|---|---|---|
| Philosophy | Simple, macOS-integrated | Traditional Unix, self-contained |
| Installation prefix | /opt/homebrew (AS) or /usr/local (Intel) | /opt/local |
| Sudo required | No | Yes |
| Default installation | Pre-built bottles | Build from source |
| Build time | Fast (bottles) | Slow (compilation) |
| Build customization | Limited options | Variants system |
| Package count | ~6,000 formulae + ~5,000 casks | ~27,000 ports |
| GUI apps | Casks | Some available |
| System integration | Uses macOS libraries | Self-contained |
| Updates | Rolling | Rolling |
| Community | Very active | Active |
| Documentation | Excellent | Good |
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
| Tool | Mechanism | Virtual Envs | Complexity |
|---|---|---|---|
| pyenv | Shims | Via pyenv-virtualenv | Medium |
| Homebrew | Cellar | Via venv | Low |
| Conda | Environment | Built-in | High |
| asdf | Shims | Via plugin | Medium |
Recommendation: pyenv for most Python development. Conda for data science.
Ruby: rbenv vs rvm vs chruby
| Feature | rbenv | rvm | chruby |
|---|---|---|---|
| Mechanism | Shims | PATH modification | PATH modification |
| Gemsets | No (use Bundler) | Yes | No |
| Complexity | Low | High | Very low |
| Shell startup | Fast | Slower | Fastest |
| Features | Minimal | Full-featured | Minimal |
| Documentation | Good | Extensive | Basic |
Recommendation: rbenv for most users. chruby for minimalists. rvm if you need gemsets.
Node.js: nvm vs fnm vs volta
| Feature | nvm | fnm | volta |
|---|---|---|---|
| Implementation | Shell script | Rust | Rust |
| Speed | Slow startup | Fast | Fast |
| Windows | No | Yes | Yes |
| .nvmrc support | Yes | Yes | No (uses package.json) |
| Package manager pinning | No | No | Yes |
| Maturity | Very mature | Mature | Newer |
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
| Scenario | Recommendation |
|---|---|
| Docker | OS package manager in container |
| Bare metal | MacPorts (isolation) or native packages |
| CI runners | Match 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
Full-Featured Setup
# 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
| Need | Solution |
|---|---|
| System tools | Homebrew |
| GUI apps | Homebrew Casks |
| Multiple Python versions | pyenv |
| Multiple Ruby versions | rbenv |
| Multiple Node versions | fnm |
| All languages | asdf or mise |
| Build customization | MacPorts |
| Isolation | MacPorts |
| Enterprise/server | MacPorts or native |
Best Practices
- Don’t mix Homebrew and MacPorts for the same package
- Use version managers for languages you develop in
- Document your setup in project READMEs
- Use Brewfile for reproducible tool installation
- Keep things simple - more tools ≠ better
- Match CI environment to local development
Recommended Stack
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.dscripts (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:
| Function | Traditional Unix | macOS |
|---|---|---|
| Init system | init, systemd | launchd |
| Service manager | systemd, rc.d | launchd |
| Task scheduler | cron | launchd |
| Socket activation | inetd, xinetd | launchd |
| Session management | PAM, login | launchd |
The Boot Process
When macOS boots:
- Firmware (iBoot on Apple Silicon) loads the kernel
- Kernel starts and launches
/sbin/launchdas PID 1 - launchd reads system configuration from:
/System/Library/LaunchDaemons/(Apple services)/Library/LaunchDaemons/(third-party system services)
- launchd starts essential system services
- loginwindow appears (GUI login)
- User session launches per-user launchd
- 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
LaunchDaemonsdirectories - 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
LaunchAgentsdirectories - Can access GUI, user session
# User agents location
$ ls ~/Library/LaunchAgents/
com.example.myagent.plist
...
Which to Use?
| Need | Use |
|---|---|
| Run before user login | Daemon |
| Access user session | Agent |
| Run as root | Daemon |
| Run as current user | Agent |
| System-wide service | Daemon |
| Per-user service | Agent |
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:
| Domain | Description | Example |
|---|---|---|
| system | System-wide services | system/com.apple.mds |
| user | Per-user services | user/501/com.example.agent |
| gui | GUI session services | gui/501/com.apple.Dock |
| login | Login session | login/501 |
| pid | Per-process | pid/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
| Key | Purpose |
|---|---|
| Label | Unique identifier (required) |
| ProgramArguments | Command to run |
| Program | Simple program path |
| RunAtLoad | Start when loaded |
| KeepAlive | Restart if exits |
| StartInterval | Run every N seconds |
| StartCalendarInterval | Run at specific times |
| WatchPaths | Run when paths change |
| QueueDirectories | Run when directories have files |
| StandardOutPath | Redirect stdout |
| StandardErrorPath | Redirect stderr |
| WorkingDirectory | Set working directory |
| EnvironmentVariables | Set environment |
| UserName | Run as user (daemons) |
| GroupName | Run 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:
| Concept | Description |
|---|---|
| PID 1 | First process, parent of all |
| Daemon | System-wide background service |
| Agent | Per-user background service |
| On-demand | Services start when needed |
| Domain | Organizational scope |
| plist | XML/binary configuration |
| launchctl | Command-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:
| Format | Extension | Human Readable | Performance |
|---|---|---|---|
| XML | .plist | Yes | Slower |
| Binary | .plist | No | Faster |
| JSON | .json | Yes | Medium |
# 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
| Type | XML Element | Example |
|---|---|---|
| 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>
Full-Featured 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.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:
- Open plist file in Xcode
- Visual editor shows keys and values
- 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:
| Tool | Use Case |
|---|---|
plutil | Validate, convert formats |
defaults | Read/write app preferences |
PlistBuddy | Complex 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?
| Requirement | Use |
|---|---|
| Run before any user logs in | Daemon |
| Access user’s GUI or files | Agent |
| Run as root | Daemon |
| Run as current user | Agent |
| System-wide scope | Daemon |
| Per-user scope | Agent |
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
| Problem | Solution |
|---|---|
| Job won’t load | Check plist syntax with plutil |
| Job loads but doesn’t run | Check Program path exists |
| Job runs but fails | Check logs, run manually |
| Permission denied | Check script is executable |
| Wrong environment | Add EnvironmentVariables |
Summary
Creating launchd jobs:
- Choose type: Agent (user) or Daemon (system)
- Create script: Executable, tested manually
- Create plist: Valid XML, required keys
- Set permissions: 644, correct ownership
- Load:
launchctl load - Test:
launchctl start, check logs - 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
| Domain | Syntax | Description |
|---|---|---|
| system | system | System services |
| user | user/<uid> | User services |
| gui | gui/<uid> | GUI session services |
| login | login/<uid> | Login services |
| pid | pid/<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
| Task | Command |
|---|---|
| Load agent | launchctl load ~/Library/LaunchAgents/X.plist |
| Load daemon | sudo launchctl load /Library/LaunchDaemons/X.plist |
| Unload | launchctl unload /path/to/plist |
| Start | launchctl start label |
| Stop | launchctl stop label |
| List all | launchctl list |
| List one | launchctl list label |
| Job info | launchctl print gui/$(id -u)/label |
| Enable | launchctl enable user/$(id -u)/label |
| Disable | launchctl disable user/$(id -u)/label |
| Force start | launchctl kickstart gui/$(id -u)/label |
| Set env | launchctl setenv VAR value |
| Get env | launchctl 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 servicesfor 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
| Column | Meaning |
|---|---|
| PID | Process ID |
| PPID | Parent Process ID |
| %CPU | CPU usage percentage |
| %MEM | Memory usage percentage |
| VSZ | Virtual memory size |
| RSS | Resident set size (physical memory) |
| TT | Controlling terminal |
| STAT | Process state |
| TIME | CPU time consumed |
Process States
| State | Meaning |
|---|---|
| R | Running |
| S | Sleeping (interruptible) |
| U | Uninterruptible sleep |
| I | Idle |
| T | Stopped |
| Z | Zombie |
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
| Tab | Shows |
|---|---|
| CPU | Processor usage |
| Memory | RAM usage |
| Energy | Battery impact |
| Disk | I/O operations |
| Network | Network 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
| Task | CLI | GUI |
|---|---|---|
| Quick process list | ps aux | Activity Monitor |
| Real-time monitoring | top, htop | Activity Monitor |
| Kill process | kill <pid> | Select → Quit |
| Find resource hog | top -o cpu | Sort by CPU column |
| Analyze process | lsof -p <pid> | Double-click → Open Files |
| Sample/profile | sample <pid> | View → Sample |
| System overview | vm_stat, iostat | Activity 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:
| Tool | Purpose |
|---|---|
ps | Snapshot of processes |
top | Real-time monitoring |
htop | Enhanced top |
kill | Send signals to processes |
pgrep/pkill | Find/kill by name |
lsof | Open files and ports |
nice/renice | Process priority |
sample | Stack trace sampling |
| Activity Monitor | GUI all-in-one |
Best practices:
- Use
topor Activity Monitor for interactive monitoring - Use
psfor scripting and one-time checks - Always try
killbeforekill -9 - Use
lsofto 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
| Key | Range | Description |
|---|---|---|
| Month | 1-12 | Month of year |
| Day | 1-31 | Day of month |
| Weekday | 0-7 | Day of week (0 and 7 = Sunday) |
| Hour | 0-23 | Hour of day |
| Minute | 0-59 | Minute 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
- No on-demand loading: Unlike launchd, cron always runs
- No power management: Doesn’t integrate with sleep/wake
- No GUI access: Can’t interact with user session
- 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
| Feature | launchd | cron |
|---|---|---|
| On-demand loading | Yes | No |
| Power management | Yes | No |
| Resource limits | Yes | No |
| GUI session access | Yes (agents) | No |
| Path watching | Yes | No |
| Network conditions | Yes | No |
| Logging | Unified log | Manual |
| Complex schedules | StartCalendarInterval | More 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:
| Method | Best For |
|---|---|
| launchd | Most macOS scheduling needs |
| cron | Simple, cross-platform scripts |
| at | One-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
| Mechanism | Use Case | API Level |
|---|---|---|
| XPC | Modern Apple IPC | High-level |
| Mach ports | Kernel-level IPC | Low-level |
| Unix sockets | Network-style IPC | POSIX |
| Pipes | Parent-child communication | POSIX |
| Shared memory | High-performance data sharing | POSIX |
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
| Type | Description |
|---|---|
| XPC Service | Bundled in application, private |
| Launch Daemon | System-wide, runs as root |
| Launch Agent | Per-user, runs as user |
| Mach Service | Registered 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:
-
Service not launching
$ log show --predicate 'subsystem == "com.apple.xpc" AND eventMessage CONTAINS "error"' --last 1h -
Entitlement issues
$ codesign -d --entitlements :- /path/to/app -
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
- Code signing validation: XPC verifies code signatures
- Entitlements: Services can require specific entitlements
- Sandboxing: XPC respects sandbox boundaries
- 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:
- XPC Service Target: Bundle containing the service
- Info.plist: Service configuration
- NSXPCConnection: Client-side API
- 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:
| Method | Complexity | Use Case |
|---|---|---|
| XPC | High | Modern Apple development |
| Mach ports | Very high | Kernel/system services |
| Unix sockets | Medium | Network-style IPC |
| Named pipes | Low | Simple shell scripts |
| Files/fswatch | Low | Quick 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 showandlsoffor 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
| Feature | launchd | systemd | SysVinit |
|---|---|---|---|
| PID 1 | Yes | Yes | Yes |
| On-demand activation | Yes | Yes | No |
| Socket activation | Yes | Yes | Via inetd |
| Timer units | Via plist | Timer units | Via cron |
| Configuration | XML plists | INI-like units | Shell scripts |
| Dependency management | Implicit | Explicit | Manual |
| Cgroup integration | No | Yes | No |
| Container support | No | Yes | No |
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
| Task | systemd | launchd |
|---|---|---|
| Start service | systemctl start foo | launchctl start foo |
| Stop service | systemctl stop foo | launchctl stop foo |
| Enable at boot | systemctl enable foo | launchctl load -w |
| Disable | systemctl disable foo | launchctl unload -w |
| Check status | systemctl status foo | launchctl list foo |
| List services | systemctl list-units | launchctl list |
| View logs | journalctl -u foo | log show --predicate 'process=="foo"' |
| Reload config | systemctl daemon-reload | Automatic |
Service Locations
| Type | systemd | launchd |
|---|---|---|
| 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 processforking: Forks and exitsoneshot: Runs oncenotify: Uses sd_notifydbus: D-Bus activated
launchd determines type implicitly:
RunAtLoad: Starts immediatelyKeepAlive: Respawns if exitsStartInterval: PeriodicWatchPaths: 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
- Service name: Use reverse-domain notation (com.example.service)
- ExecStart: Use
ProgramArgumentsarray - Restart=always: Use
KeepAlive - After=network.target: Remove (launchd handles implicitly)
- Environment: Use
EnvironmentVariablesdict - Install section: Use
RunAtLoador 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:
| SysVinit | launchd Equivalent |
|---|---|
/etc/init.d/foo start | launchctl start foo |
/etc/init.d/foo stop | launchctl stop foo |
chkconfig foo on | launchctl load -w |
update-rc.d foo defaults | Place plist in LaunchDaemons |
/etc/rc.d/rc.local | Use 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:
| Concept | systemd | launchd |
|---|---|---|
| Service definition | .service | .plist |
| Service manager | systemctl | launchctl |
| Logging | journald | Unified log |
| Timers | .timer units | StartCalendarInterval |
| Sockets | .socket units | Sockets dict in plist |
| User services | user units | LaunchAgents |
| System services | system units | LaunchDaemons |
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:
| Task | macOS (BSD) | Linux (GNU) |
|---|---|---|
| In-place sed | sed -i '' 's/a/b/' | sed -i 's/a/b/' |
| Extended regex in grep | grep -E | grep -E or grep -P |
| Copy to clipboard | pbcopy | xclip or xsel |
| Open file with default app | open file.pdf | xdg-open file.pdf |
| Find files by content | mdfind "query" | locate or find + grep |
| Colored ls | ls -G | ls --color |
| Date formatting | date -j -f | date -d |
| Network interfaces | ifconfig, networksetup | ip addr |
| Service management | launchctl | systemctl |
The Solutions
You have several approaches:
- Learn both: Know the BSD and GNU syntax for common operations
- Install GNU tools: Use Homebrew to get GNU coreutils
- Write portable scripts: Use POSIX-compatible syntax that works everywhere
- 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
readlink: Getting Real Paths
# 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
| Command | GNU (Linux) | BSD (macOS) |
|---|---|---|
| In-place sed | sed -i 's/a/b/' | sed -i '' 's/a/b/' |
| Perl regex grep | grep -P '\d+' | Not available |
| Colored ls | ls --color | ls -G |
| Date parsing | date -d "string" | date -j -f "fmt" "str" |
| Relative date | date -d "+7 days" | date -v+7d |
| File stats | stat -c %s file | stat -f %z file |
| Version sort | sort -V | Not available |
| Human size sort | sort -h | Not available |
| Canonical path | readlink -f | realpath (macOS 12.3+) |
| xargs parallel | xargs -P 4 | Not available |
Recommendations
- For scripts: Use POSIX-compatible syntax when possible
- For interactive use: Install GNU coreutils via Homebrew
- For maximum portability: Test on both platforms
- 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 Command | Purpose | Linux Equivalent |
|---|---|---|
pbcopy/pbpaste | Clipboard access | xclip, xsel |
open | Launch files/apps | xdg-open |
mdfind | Search file contents | locate, grep -r |
mdls | View file metadata | stat, file |
say | Text-to-speech | espeak, festival |
screencapture | Screenshots | gnome-screenshot, scrot |
networksetup | Network config | nmcli, ip |
scutil | System config | Various tools |
defaults | Preferences | gsettings, dconf |
caffeinate | Prevent sleep | systemd-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 Package | Commands Included | g-prefix Examples |
|---|---|---|
| coreutils | ls, cp, mv, rm, date, cat, chmod, etc. | gls, gcp, gmv, grm, gdate |
| gnu-sed | sed | gsed |
| grep | grep, egrep, fgrep | ggrep |
| gawk | awk | gawk |
| findutils | find, xargs, locate | gfind, gxargs, glocate |
| gnu-tar | tar | gtar |
| gnu-which | which | gwhich |
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
Recommended: Install gawk
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
| Feature | GNU | BSD | Portable Alternative |
|---|---|---|---|
| sed -i | sed -i | sed -i '' | temp file |
| sed \n | Works | Doesn’t work | $‘\n’ or literal |
| grep -P | Works | Doesn’t exist | Use -E with POSIX |
| sort -V | Works | Doesn’t exist | Custom solution |
| sort -h | Works | Doesn’t exist | Sort raw bytes |
| cut –complement | Works | Doesn’t exist | Use awk |
| awk IGNORECASE | Works | Doesn’t exist | Character 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
| Attribute | Purpose |
|---|---|
com.apple.quarantine | Downloaded file, triggers Gatekeeper |
com.apple.lastuseddate#PS | Last opened date |
com.apple.metadata:kMDItemWhereFroms | Download URL |
com.apple.FinderInfo | Finder metadata (labels, etc.) |
com.apple.ResourceFork | Classic 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
ln: Creating Links
# 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 Link Behavior
# 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
| Task | Best Command | Notes |
|---|---|---|
| Copy files | ditto or cp -p | ditto preserves everything |
| Sync directories | rsync -av | Use Homebrew rsync for xattr support |
| Large file copy | rsync --progress | Shows progress |
| Move to Trash | trash (brew) | Safer than rm |
| Remove quarantine | xattr -d com.apple.quarantine | Common need |
| Archive directory | ditto -c -k | Creates ZIP with metadata |
| Strip metadata | xattr -cr | Before sharing |
| Find files | mdfind | Uses 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 Command | macOS Equivalent | Notes |
|---|---|---|
ip addr | ifconfig | ifconfig is deprecated on Linux |
ip link | ifconfig | Same |
ip route | netstat -rn or route | |
ss | netstat or lsof -i | |
systemctl restart network | networksetup | No systemd on macOS |
nmcli | networksetup | macOS equivalent |
iwconfig/iwlist | airport | Wi-Fi utility |
hostnamectl | scutil --get/set HostName | |
resolvectl | scutil --dns |
Key macOS-specific tools:
networksetup: High-level network configurationscutil: System configuration accessairport: Wi-Fi diagnosticsdscacheutil: Directory services cachenetworkQuality: 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
| Information | Command |
|---|---|
| macOS version | sw_vers -productVersion |
| Kernel version | uname -r |
| Architecture | uname -m |
| CPU info | sysctl -n machdep.cpu.brand_string |
| CPU cores | sysctl -n hw.ncpu |
| Memory | sysctl -n hw.memsize |
| Model | sysctl -n hw.model |
| Serial number | ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/Serial/{print $4}' |
| Hardware details | system_profiler SPHardwareDataType |
| Disk space | df -h |
| Memory stats | vm_stat |
| Battery | system_profiler SPPowerDataType |
| Uptime | uptime |
| Processes | ps aux or top |
| Network config | ifconfig 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}')
Handling readlink Differences
# 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:
- Shebang: Use
#!/usr/bin/env bashor#!/bin/shfor POSIX - sed -i: Use temp files or detect OS
- grep: Avoid
-P, use-Ewith POSIX classes - date: Abstract into functions
- stat: Use
wc -corlsfor file size - readlink -f: Provide fallback function
- Arrays: Avoid associative arrays for Bash 3.2 compatibility
- Test: Test on both macOS and Linux
- Lint: Run shellcheck
Common compatibility functions:
| Task | Portable Approach |
|---|---|
| In-place sed | Temp file or OS detection |
| File size | wc -c < file |
| Canonical path | Custom function |
| Date math | OS-specific functions |
| Clipboard | OS detection (pbcopy vs xclip) |
| Open file | OS detection (open vs xdg-open) |
| Command check | command -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
| Aspect | Linux | macOS |
|---|---|---|
| Compiler | GCC (usually) | Clang/LLVM (always) |
| Default shell | Bash | Zsh |
| Shared libraries | .so files | .dylib files |
| Library path | LD_LIBRARY_PATH | DYLD_LIBRARY_PATH |
| Binary inspection | ldd, readelf | otool, nm |
| Debugger | GDB | LLDB |
| Package format | ELF | Mach-O |
| Frameworks | N/A | Native concept |
| Multiple architectures | Separate binaries | Universal 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:
- Have Terminal.app or another terminal emulator ready
- Be comfortable with basic command-line operations
- 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/homebrewvs/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
| Tool | Description |
|---|---|
clang | C, C++, Objective-C compiler |
clang++ | C++ compiler (same as clang with C++ mode) |
swift | Swift compiler |
ld | The linker |
as | Assembler |
ar | Archive tool (static libraries) |
libtool | Library creation tool |
make | Build automation |
cmake | Cross-platform build system (recent versions) |
Development Utilities
| Tool | Description |
|---|---|
git | Version control |
svn | Subversion (legacy) |
lldb | Debugger |
nm | Symbol table viewer |
otool | Object file viewer |
lipo | Universal binary tool |
codesign | Code signing |
install_name_tool | Library path modifier |
dsymutil | Debug symbol utility |
dwarfdump | DWARF debug info viewer |
strip | Remove symbols from binaries |
size | Display 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
| Tool | Description |
|---|---|
xcrun | Run tools from active developer directory |
xcode-select | Manage developer tool paths |
xcodebuild | Build Xcode projects (limited without full Xcode) |
instruments | Command-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:
- Visit developer.apple.com/download/more/
- Sign in with Apple ID (free account works)
- Search for “Command Line Tools”
- Download the .dmg for your macOS version
- 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:
- Command Line Tools only (standalone)
- 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:
| Scenario | Use |
|---|---|
| Building iOS apps | Xcode |
| Homebrew formula development | Command Line Tools (often) |
| Compiling Unix software | Either works |
| Using different SDK version | Switch as needed |
| CI/CD server | Command 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
| Task | Command |
|---|---|
| Install CLI tools | xcode-select --install |
| Check installation path | xcode-select -p |
| Switch to Xcode | sudo xcode-select -s /Applications/Xcode.app/Contents/Developer |
| Switch to CLI tools | sudo xcode-select -s /Library/Developer/CommandLineTools |
| Reset to default | sudo xcode-select -r |
| Find tool path | xcrun -f <tool> |
| Get SDK path | xcrun --show-sdk-path |
| Run tool with SDK | xcrun --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
| Feature | GCC | Clang |
|---|---|---|
| Default standard | -std=gnu17 | -std=gnu17 |
| Warning flags | GCC-specific available | Mostly compatible |
| Error messages | Good | Excellent (clearer) |
| Extensions | GCC extensions | GCC + Clang extensions |
| Static analysis | -fanalyzer | --analyze |
| Sanitizers | Available | Better integration |
| Modules | Limited | Better 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
| Flag | Description |
|---|---|
-Wall | Common warnings |
-Wextra | Additional warnings |
-Wpedantic | Strict ISO compliance |
-Wconversion | Implicit conversions |
-Wshadow | Variable shadowing |
-Wformat=2 | Format string issues |
-Wnull-dereference | Null pointer dereference |
-Wuninitialized | Uninitialized 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
| Macro | Description |
|---|---|
__APPLE__ | Always defined on Apple platforms |
__MACH__ | Mach kernel (macOS, iOS) |
TARGET_OS_MAC | macOS (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:
| Aspect | Detail |
|---|---|
gcc command | Runs Apple Clang, not GCC |
| Apple Clang | Modified LLVM with Apple extensions |
| Compatibility | Most GCC flags work |
| Advantages | Better errors, sanitizers, static analysis |
| Real GCC | Available via Homebrew as gcc-14 |
| Sanitizers | ASan, UBSan, TSan available |
| Static analysis | clang --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
| Option | Purpose |
|---|---|
--prefix=PATH | Installation directory |
--with-PACKAGE=PATH | Path to dependency |
--without-PACKAGE | Disable optional feature |
--enable-FEATURE | Enable optional feature |
--disable-FEATURE | Disable feature |
--build=TRIPLE | Build system type |
--host=TRIPLE | Target 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
| Prefix | Use |
|---|---|
/usr/local | Traditional Unix (less common now) |
/opt/homebrew | Homebrew on Apple Silicon |
/opt/local | MacPorts |
$HOME/.local | User-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:
-
Check prerequisites
$ xcode-select -p $ brew doctor -
Verify dependencies are installed
$ pkg-config --exists libname && echo "Found" -
Check config.log
$ tail -100 config.log # Shows why configure failed -
Search for macOS-specific issues
- Check project’s issue tracker
- Search Homebrew formula
- Check MacPorts portfile
-
Try Homebrew’s formula
$ brew install --verbose software-name # Observe how Homebrew builds it -
Check architecture
$ uname -m $ file /opt/homebrew/lib/libdependency.dylib
Summary
Building from source on macOS:
| Aspect | Consideration |
|---|---|
| Compiler | Clang (not GCC) |
| Headers | May need explicit paths |
| Libraries | Often in /opt/homebrew |
| pkg-config | Set PKG_CONFIG_PATH |
| GNU tools | Install via Homebrew if needed |
| Architecture | arm64 or x86_64 |
| Installation | /usr/local or custom prefix |
| SIP | Can’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
| Platform | Extension | Format |
|---|---|---|
| macOS | .dylib | Mach-O |
| Linux | .so | ELF |
| Windows | .dll | PE |
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:
DYLD_LIBRARY_PATH(if set and SIP disabled)LD_LIBRARY_PATH(fallback, if set)- Paths embedded in the binary (rpath, @executable_path, etc.)
/usr/lib/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
- Library is built with an install name
- When you link against the library, the install name is copied to your binary
- 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
| Tool | Purpose |
|---|---|
otool -L | View linked libraries |
otool -D | View install name |
otool -l | View load commands |
nm | View symbols |
install_name_tool | Modify paths |
dyld_info | Modern binary analysis |
file | Check architecture |
| Path Prefix | Meaning |
|---|---|
@executable_path | Relative to main executable |
@loader_path | Relative to loading binary |
@rpath | Search rpath list |
Key differences from Linux:
| Aspect | Linux | macOS |
|---|---|---|
| Extension | .so | .dylib |
| Path variable | LD_LIBRARY_PATH | DYLD_LIBRARY_PATH |
| View dependencies | ldd | otool -L |
| Modify paths | patchelf | install_name_tool |
| Embedded path | RPATH/RUNPATH | Install 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
| Aspect | Unix Library | macOS Framework |
|---|---|---|
| Structure | Single file + separate headers | Bundle directory |
| Headers | /usr/include or /usr/local/include | Inside framework |
| Versioning | Symlinks (libfoo.so.1) | Versions/ directory |
| Resources | N/A | Included |
| Self-contained | No | Yes |
| Discoverability | pkg-config, manual | Built-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
| Framework | Purpose |
|---|---|
| CoreFoundation | C-level foundation (strings, collections) |
| Foundation | Objective-C foundation (NSObject, etc.) |
| AppKit | macOS GUI |
| UIKit | iOS/iPadOS GUI |
| Security | Cryptography, keychain |
| CoreGraphics | 2D graphics |
| CoreData | Object persistence |
| CoreML | Machine 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:
-Fspecified pathsFRAMEWORK_SEARCH_PATHSin Xcode- 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
| Aspect | Framework | dylib |
|---|---|---|
| Structure | Bundle (directory) | Single file |
| Headers | Included | Separate |
| Resources | Supported | N/A |
| Versioning | Built-in | Symlinks |
| Compile flag | -framework Name | -lname |
| Search path | -F/path | -L/path |
| Apple APIs | Required | Possible |
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
| Feature | GDB | LLDB |
|---|---|---|
| Platform | Linux, others | macOS, iOS, LLVM |
| Project | GNU | LLVM |
| On macOS | Available via Homebrew | Built-in |
| Scripting | Python, Guile | Python |
| Default on macOS | No | Yes |
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
| Task | LLDB | GDB |
|---|---|---|
| Run program | run or r | run |
| Run with args | run arg1 arg2 | run arg1 arg2 |
| Continue | continue or c | continue |
| Step over | next or n | next |
| Step into | step or s | step |
| Step out | finish | finish |
| Stop | Ctrl+C | Ctrl+C |
| Quit | quit or q | quit |
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
| Task | GDB | LLDB |
|---|---|---|
| Run | run | run |
| Break at function | break main | b main |
| Break at line | break file:line | b file:line |
| Continue | continue | continue |
| Step over | next | next |
| Step into | step | step |
| Step out | finish | finish |
print var | print var | |
| Print hex | print/x var | p/x var |
| Backtrace | backtrace | bt |
| List breakpoints | info breakpoints | br list |
| Delete breakpoint | delete 1 | br del 1 |
| Examine memory | x/10x addr | x -c10 -fx addr |
| Local variables | info locals | fr v |
| Disassemble | disas | disassemble |
| Registers | info registers | register read |
| Attach | attach pid | attach -p pid |
| Set variable | set var x=10 | expr 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
| Category | Key Commands |
|---|---|
| Running | run, continue, next, step, finish |
| Breakpoints | b function, b file:line, br list, br del |
| Inspection | print var, frame variable, bt, memory read |
| Threads | thread list, thread select, bt all |
| Memory | x address, memory read |
| Registers | register read, register write |
| Control | process 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
| Tool | Purpose | Best For |
|---|---|---|
time | Basic timing | Quick measurements |
sample | CPU sampling | Quick CPU profile |
spindump | Hang analysis | Unresponsive apps |
leaks | Memory leaks | Memory debugging |
heap | Heap analysis | Memory usage |
vmmap | Memory map | Virtual memory |
fs_usage | File system | I/O debugging |
dtrace | Dynamic tracing | Advanced profiling |
| Instruments | GUI profiling | Comprehensive 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:
- Boot to Recovery Mode
csrutil disable(security risk)- 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
| Template | Purpose |
|---|---|
| Time Profiler | CPU usage sampling |
| Allocations | Memory allocation tracking |
| Leaks | Memory leak detection |
| System Trace | Comprehensive system events |
| File Activity | File system operations |
| Network | Network connections |
| Core Data | Core Data performance |
| Animation Hitches | UI performance |
| Metal System Trace | GPU profiling |
Using Time Profiler
- Open Instruments
- Choose “Time Profiler”
- Select target (app or process)
- Click Record
- Exercise your code
- Click Stop
- 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
- Build with optimizations (to profile realistic code)
- But keep debug symbols (
-gflag) - Use Release configuration, not Debug
- Have representative test data
During Profiling
- Minimize background activity
- Close unnecessary applications
- Run multiple trials
- 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
- Quick timing: Use
time - CPU hotspots: Use
sample - Memory issues: Use
leaks,heap,vmmap - I/O problems: Use
fs_usage - 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
| Tool | What It Shows | When to Use |
|---|---|---|
time | Execution time | Quick benchmarks |
sample | CPU call stacks | Finding hot functions |
spindump | Hang state | App freezes |
leaks | Memory leaks | Memory debugging |
heap | Heap contents | Memory optimization |
vmmap | Memory map | Memory layout |
fs_usage | File operations | I/O debugging |
dtrace | Dynamic tracing | Advanced analysis |
| Instruments | Everything | Comprehensive 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
| Period | Mac Architecture | Identifier |
|---|---|---|
| 2006-2020 | Intel 64-bit | x86_64 |
| 2020-present | Apple Silicon | arm64 |
| Transition | Both | Universal |
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:
| Architecture | Prefix |
|---|---|
| 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
| Task | Command |
|---|---|
| Check architecture | file binary or lipo -info binary |
| Build universal | clang -arch arm64 -arch x86_64 ... |
| Combine binaries | lipo -create arm64_bin x86_64_bin -output universal |
| Extract architecture | lipo -thin arm64 universal -output arm64_only |
| Run as Intel | arch -x86_64 ./program |
| Check if translated | sysctl -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:
| Aspect | Traditional Unix | macOS |
|---|---|---|
| User management | /etc/passwd, useradd | Directory Services, dscl, sysadminctl |
| System permissions | Standard Unix permissions | Unix permissions + ACLs + SIP |
| Firewall | iptables, nftables | pf (from OpenBSD) |
| Logging | syslog, journald | Unified Logging (log command) |
| Configuration | Text config files | Property lists (plists), defaults |
| Startup | systemd, rc.d | launchd (covered in Part V) |
| Recovery | Single-user mode | Recovery 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:
| Command | Purpose |
|---|---|
dscl | Directory Service command line utility |
sysadminctl | Modern user management tool (10.10+) |
dseditgroup | Group membership management |
dscacheutil | Query and flush directory cache |
id | Display 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 Range | Purpose |
|---|---|
| 0 | root |
| 1-199 | System accounts (daemon, nobody, etc.) |
| 200-400 | System services |
| 401-500 | Reserved |
| 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
Using sysadminctl (Recommended)
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:
| Group | GID | Purpose |
|---|---|---|
| wheel | 0 | Traditional Unix superuser group |
| admin | 80 | macOS administrators (can use sudo) |
| staff | 20 | Default group for regular users |
| everyone | 12 | All users including guests |
| localaccounts | 61 | All local (non-network) accounts |
| _developer | 204 | Can 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
| Task | Linux | macOS |
|---|---|---|
| List users | cat /etc/passwd | dscl . -list /Users |
| Add user | useradd | sysadminctl -addUser |
| Delete user | userdel | sysadminctl -deleteUser |
| Modify user | usermod | dscl . -change |
| Add to group | usermod -aG group user | dseditgroup -o edit -a user -t user group |
| List groups | cat /etc/group | dscl . -list /Groups |
| Change password | passwd user | dscl . -passwd /Users/user |
| User info | id user | id 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:
| Command | Purpose |
|---|---|
dscl . -list /Users | List all users |
dscl . -read /Users/name | Read user details |
sysadminctl -addUser | Create new user |
sysadminctl -deleteUser | Delete user |
dseditgroup -o edit -a user -t user group | Add to group |
id username | Show UID/GIDs |
dscacheutil -flushcache | Clear 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--:
| Position | Meaning |
|---|---|
| 1 | File type (- file, d directory, l symlink) |
| 2-4 | Owner permissions (user) |
| 5-7 | Group permissions |
| 8-10 | Other permissions (everyone else) |
Permission bits:
| Symbol | Octal | Meaning for Files | Meaning for Directories |
|---|---|---|---|
| r | 4 | Read contents | List contents |
| w | 2 | Modify contents | Create/delete files |
| x | 1 | Execute | Enter (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 usergroup:groupname- Specific group
Actions:
allow- Grant permissiondeny- Explicitly deny permission
Permissions for files:
| Permission | Meaning |
|---|---|
| read | Read file contents |
| write | Modify file contents |
| execute | Execute file |
| delete | Delete file |
| append | Append to file |
| readattr | Read attributes |
| writeattr | Write attributes |
| readextattr | Read extended attributes |
| writeextattr | Write extended attributes |
| readsecurity | Read ACL |
| writesecurity | Modify ACL |
| chown | Change ownership |
Permissions for directories:
| Permission | Meaning |
|---|---|
| list | List directory contents |
| search | Access files in directory |
| add_file | Create files |
| add_subdirectory | Create subdirectories |
| delete_child | Delete items in directory |
| readattr | Read attributes |
| writeattr | Write attributes |
| readextattr | Read extended attributes |
| writeextattr | Write extended attributes |
| readsecurity | Read ACL |
| writesecurity | Modify ACL |
| chown | Change 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:
| Flag | Meaning |
|---|---|
| file_inherit | Apply to new files |
| directory_inherit | Apply to new subdirectories |
| limit_inherit | Don’t propagate beyond direct children |
| only_inherit | Don’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
| Feature | Linux | macOS |
|---|---|---|
| Basic permissions | Same | Same |
| View ACLs | getfacl | ls -le |
| Set ACLs | setfacl | chmod +a |
| ACL format | POSIX ACLs | NFSv4 ACLs |
| Extended attributes | getfattr/setfattr | xattr |
| File flags | chattr | chflags |
| Mandatory access | SELinux, AppArmor | SIP, Sandbox |
Summary
macOS permissions combine multiple systems:
| Layer | Tool | Purpose |
|---|---|---|
| Unix permissions | chmod, chown | Basic access control |
| ACLs | chmod +a, ls -le | Fine-grained access |
| Extended attributes | xattr | Metadata (quarantine, etc.) |
| File flags | chflags | Special protection |
| SIP | csrutil | System 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:
- Installing specific kernel extensions (legacy drivers)
- Security research (analyzing malware, reverse engineering)
- Modifying system behavior for development
- 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
- Restart your Mac
- Hold Command + R during startup to boot into Recovery Mode
- From the menu bar, select Utilities > Terminal
- 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
- Restart:
reboot
Apple Silicon Macs
- Shut down your Mac completely
- Press and hold the Power button until “Loading startup options” appears
- Click Options, then Continue
- If prompted, select a user and enter their password
- From the menu bar, select Utilities > Terminal
- 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
- Restart
Re-enabling SIP
Always re-enable SIP when you’re done:
- Boot into Recovery Mode (same method as above)
- Open Terminal
- Enable SIP:
$ csrutil enable
Successfully enabled System Integrity Protection. Please restart the machine for the changes to take effect.
- 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
csrutiloutside 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:
- A newer version that works with SIP
- A System Extension (modern kext replacement)
- 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
- Keep SIP enabled for daily use
- Use /usr/local for custom software
- Use Homebrew instead of modifying system paths
- 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
- Never disable SIP on production machines
- Consider virtualization for testing that requires SIP disabled
Summary
SIP is a fundamental security feature of modern macOS:
| Aspect | Details |
|---|---|
| Purpose | Protect system integrity from malware and mistakes |
| Protected | /System, /usr, /bin, /sbin, kernel, system processes |
| Unprotected | /usr/local, /Applications, /Library, ~/ |
| Check status | csrutil status |
| Disable | Recovery Mode only |
| Best practice | Keep 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:
| Feature | pf (macOS) | iptables (Linux) |
|---|---|---|
| Syntax | English-like | Command flags |
| Config file | /etc/pf.conf | /etc/iptables/rules.v4 |
| Rule order | Last match wins | First match wins |
| Tables | Built-in support | ipset (separate) |
| NAT | Integrated | Separate chain |
| Logging | Built-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:
- Macros (variables)
- Tables (IP address lists)
- Options (global settings)
- Scrub (packet normalization)
- NAT/Redirect (if needed)
- 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:
blockorpass - direction:
inorout - 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 }
Method 2: Use an Anchor (Recommended)
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:
- Application Firewall (socketfilterfw) - GUI-configurable, app-based
- 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:
| Task | Command |
|---|---|
| Enable | sudo pfctl -e |
| Disable | sudo pfctl -d |
| Load rules | sudo pfctl -f /etc/pf.conf |
| Show rules | sudo pfctl -s rules |
| Show state | sudo pfctl -s state |
| Add to table | sudo pfctl -t name -T add IP |
| Flush rules | sudo pfctl -F rules |
| Test config | sudo 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:
| Aspect | Traditional Syslog | Unified Logging |
|---|---|---|
| Storage | Plain text files | Compressed binary database |
| Performance | Disk I/O intensive | Memory-buffered, compressed |
| Query | grep through files | Structured queries |
| Metadata | Limited | Rich (subsystem, category, type) |
| Privacy | None | Built-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
| Operator | Meaning |
|---|---|
== | Equals |
!= | Not equals |
<, >, <=, >= | Comparisons |
contains | String contains |
BEGINSWITH | String starts with |
ENDSWITH | String ends with |
MATCHES | Regular expression |
AND, OR, NOT | Logical operators |
[c] | Case-insensitive modifier |
Available Predicate Fields
| Field | Description |
|---|---|
process | Process name |
processID | Process ID (PID) |
subsystem | Logging subsystem |
category | Log category |
eventMessage | The log message |
messageType | Log level (default, info, debug, error, fault) |
senderImagePath | Path to the binary that logged |
eventType | activityCreateEvent, logEvent, etc. |
Log Levels and Types
Unified logging has five log types:
| Type | Description | When to Use |
|---|---|---|
| Default | Standard messages | General information |
| Info | Informational | Helpful but verbose |
| Debug | Debugging | Development only |
| Error | Errors | Something went wrong |
| Fault | Critical | System/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
| Subsystem | Description |
|---|---|
com.apple.wifi | Wi-Fi |
com.apple.network | Networking |
com.apple.networkextension | VPN, network extensions |
com.apple.apfs | APFS filesystem |
com.apple.launchd | launchd services |
com.apple.securityd | Security daemon |
com.apple.opendirectoryd | Directory services |
com.apple.TimeMachine | Time Machine |
com.apple.Spotlight | Spotlight indexing |
com.apple.xpc | XPC services |
com.apple.coredata | Core Data |
com.apple.bluetooth | Bluetooth |
com.apple.audio | Audio |
com.apple.powerd | Power 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:
| Task | Command |
|---|---|
| Stream live logs | log stream |
| Show historical logs | log 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 logs | sudo 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:
| Type | Flag | Example |
|---|---|---|
| Boolean | -bool | true, false, yes, no, 1, 0 |
| Integer | -int | 42, 0, -1 |
| Float | -float | 0.5, 1.0, 3.14 |
| String | -string | "Hello" |
| Array | -array | Multiple values |
| Dict | -dict | Key-value pairs |
| Data | -data | Hex-encoded data |
| Date | -date | ISO 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
Menu Bar and Control Center
# 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:
| Domain | Restart Command |
|---|---|
| Finder | killall Finder |
| Dock | killall Dock |
| SystemUIServer | killall SystemUIServer |
| Safari | killall Safari |
| Other apps | killall AppName |
| Global changes | Log out and back in |
| System changes | Restart |
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:
| Task | Command |
|---|---|
| Read all preferences | defaults read domain |
| Read specific key | defaults read domain key |
| Write value | defaults write domain key -type value |
| Delete key | defaults delete domain key |
| Export preferences | defaults export domain file.plist |
| Import preferences | defaults import domain file.plist |
| List domains | defaults domains |
| Find preferences | defaults find searchterm |
Key domains:
| Domain | Purpose |
|---|---|
com.apple.finder | Finder preferences |
com.apple.dock | Dock preferences |
com.apple.Safari | Safari preferences |
NSGlobalDomain | Global/system-wide preferences |
com.apple.screencapture | Screenshot preferences |
com.apple.desktopservices | Desktop 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):
| Keys | Mode |
|---|---|
| Command + R | Recovery Mode |
| Option | Startup Manager (choose boot disk) |
| Shift | Safe Mode |
| Command + V | Verbose Mode |
| Command + S | Single User Mode (older macOS) |
| D | Apple Diagnostics |
| N | NetBoot |
| T | Target Disk Mode |
| Option + Command + R | Internet Recovery |
| Command + Option + P + R | Reset NVRAM |
Apple Silicon Macs
Different process:
| Action | How |
|---|---|
| Recovery Mode | Hold Power until “Loading startup options” |
| Startup Manager | Hold Power, then select disk |
| Safe Mode | Hold Shift during “Loading startup options” |
| Diagnostics | Hold Power, then Command + D |
| DFU Mode | Special button sequence (for restore) |
| Share Disk | In 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:
| Stage | Component | Purpose |
|---|---|---|
| 1 | Firmware (EFI/iBoot) | Hardware init, secure boot |
| 2 | Boot loader | Load kernel |
| 3 | XNU Kernel | Core OS, mount root |
| 4 | launchd (system) | Start system services |
| 5 | loginwindow | User authentication |
| 6 | launchd (user) | Start user services |
Key startup modes:
| Mode | Purpose | How |
|---|---|---|
| Recovery | Repair, reinstall | Cmd+R / Hold Power |
| Safe | Disable extensions | Shift |
| Verbose | See boot messages | Cmd+V |
| Target Disk | Share disk | T (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
- Restart your Mac (or turn it on)
- Immediately press and hold Command + R
- Keep holding until you see the Apple logo or spinning globe
- Release when you see the macOS Utilities window
Alternative boot options:
| Keys | Result |
|---|---|
| Command + R | Recovery from local partition |
| Option + Command + R | Internet Recovery (latest macOS) |
| Shift + Option + Command + R | Internet Recovery (original macOS) |
Apple Silicon Macs
- Shut down your Mac completely
- Press and hold the Power button
- Keep holding until “Loading startup options” appears
- Click Options, then Continue
- Select a user account and enter password if prompted
- 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:
- Open Disk Utility
- Select the disk/volume
- Click “First Aid”
- 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:
- Select the internal drive (container)
- Click “Erase”
- Name it, choose APFS format
- Quit Disk Utility
- 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:
- Choose “Restore from Time Machine”
- Select Time Machine disk
- Choose backup to restore
- Select destination disk
- 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:
- Utilities > Startup Security Utility
- 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)
- In Recovery Mode
- Utilities > Share Disk
- Select disk to share
- 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
- Hold Power button
- Select disk, hold Shift
- 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
- Hold Power button
- 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:
- Hold Command + S during startup
- Arrives at root shell
- Run repairs:
/sbin/fsck -fy
/sbin/mount -uw /
# Make changes
exit
Troubleshooting Boot Issues
Mac Won’t Start
- Check power: Hold power 10 seconds, release, press again
- Reset SMC (Intel):
- Shut down
- Hold Shift + Control + Option + Power for 10 seconds
- Release all keys, press power
- Reset NVRAM (Intel):
- Restart, hold Command + Option + P + R for 20 seconds
- Safe Mode: Hold Shift (eliminates software issues)
- Recovery Mode: Command + R (repair or reinstall)
- Apple Diagnostics: Hold D (hardware test)
Mac Stuck at Login
- Boot to Safe Mode (Shift)
- Remove login items:
# In Recovery Terminal
$ rm /Volumes/Macintosh\ HD/Users/username/Library/Preferences/com.apple.loginitems.plist
- 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:
| Task | Method |
|---|---|
| Access Recovery | Cmd+R (Intel) / Hold Power (AS) |
| Reset password | Recovery > Utilities > Terminal |
| Disable SIP | Recovery > Terminal > csrutil disable |
| Repair disk | Disk Utility > First Aid |
| Reinstall macOS | Recovery > Reinstall macOS |
| Restore backup | Recovery > Restore from Time Machine |
| Safe Mode | Hold Shift |
| Verbose Mode | Cmd+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
| Tool | Purpose |
|---|---|
networksetup | High-level network service configuration |
ipconfig | DHCP and IP configuration |
ifconfig | Interface configuration (BSD) |
scutil | System configuration utility |
Diagnostic Tools
| Tool | Purpose |
|---|---|
ping | Test host reachability |
traceroute | Trace packet route |
mtr | Combined ping/traceroute (via Homebrew) |
netstat | Network statistics |
nettop | Real-time network activity |
tcpdump | Packet capture |
networkQuality | Network performance test (macOS 12+) |
DNS and Discovery
| Tool | Purpose |
|---|---|
dig | DNS lookup |
host | Simple DNS lookup |
nslookup | DNS lookup (legacy) |
dscacheutil | Directory services cache |
dns-sd | DNS service discovery |
Wi-Fi Tools
| Tool | Purpose |
|---|---|
airport | Wi-Fi diagnostics (hidden utility) |
networksetup | Wi-Fi configuration |
wdutil | Wireless 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
| Task | Recommended Tool | Notes |
|---|---|---|
| View/change service settings | networksetup | Persistent, high-level |
| DHCP operations | ipconfig | Renew/release lease |
| Quick interface check | ifconfig | View status, temp changes |
| Hostname/DNS/proxy info | scutil | System config access |
| Routing | route, netstat -rn | Route table management |
Key points:
- Use
networksetupfor persistent changes that should survive reboots - Use
ipconfigfor DHCP operations like renewing leases - Use
ifconfigfor quick temporary changes or diagnostics - Use
scutilfor system-level queries and hostname configuration - Changes made with
ifconfigandrouteare 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 (Link Aggregation)
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
| Task | Command |
|---|---|
| List services | networksetup -listallnetworkservices |
| View service order | networksetup -listnetworkserviceorder |
| Change service priority | networksetup -ordernetworkservices |
| Create service | networksetup -createnetworkservice |
| Remove service | networksetup -removenetworkservice |
| List locations | networksetup -listlocations |
| Switch location | networksetup -switchtolocation |
| Create location | networksetup -createlocation |
| Create VLAN | networksetup -createVLAN |
| Create bond | networksetup -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
| Service | Type |
|---|---|
| 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:
- A DNS server that supports dynamic updates or manually added records
- 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
| Task | Command |
|---|---|
| Browse services | dns-sd -B _type._tcp local. |
| Resolve service | dns-sd -L "name" _type._tcp local. |
| Look up IP | dns-sd -G v4 hostname.local. |
| Advertise service | dns-sd -R "name" _type._tcp local. port |
| Query records | dns-sd -Q name type |
| Restart mDNSResponder | sudo killall -HUP mDNSResponder |
Key points:
- Bonjour enables automatic service discovery without configuration
- mDNS resolves
.localhostnames 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:
| Protocol | Description | Use Case |
|---|---|---|
| IKEv2 | Internet Key Exchange v2 | Modern, secure, recommended |
| L2TP/IPSec | Layer 2 Tunneling Protocol | Legacy, widely supported |
| Cisco IPSec | Cisco proprietary | Enterprise Cisco environments |
| PPTP | Point-to-Point Tunneling | Deprecated, 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
| Task | Command |
|---|---|
| List VPNs | scutil --nc list |
| Connect | scutil --nc start "VPN Name" |
| Disconnect | scutil --nc stop "VPN Name" |
| Check status | scutil --nc status "VPN Name" |
| Show details | scutil --nc show "VPN Name" |
| Monitor | scutil --nc watch "VPN Name" |
| Install profile | sudo 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:
| Service | Protocol | Port | CLI Control |
|---|---|---|---|
| Remote Login (SSH) | SSH | 22 | systemsetup, launchctl |
| Screen Sharing | VNC/ARD | 5900 | kickstart, defaults |
| File Sharing | SMB/AFP | 445/548 | sharing, defaults |
| Remote Management | ARD | 3283 | kickstart |
| Remote Apple Events | AE | 3031 | systemsetup |
| Printer Sharing | CUPS | 631 | cupsctl |
| Internet Sharing | NAT | various | defaults |
| Bluetooth Sharing | Bluetooth | - | defaults |
| Content Caching | HTTP | 49152+ | 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
| Service | Enable Command | Disable Command |
|---|---|---|
| SSH | sudo systemsetup -setremotelogin on | sudo systemsetup -setremotelogin off |
| Screen Sharing | sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist | sudo launchctl unload -w ... |
| File Sharing | sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.smbd.plist | sudo launchctl unload -w ... |
| Remote Management | sudo kickstart -activate ... | sudo kickstart -deactivate ... |
| Printer Sharing | sudo cupsctl --share-printers | sudo cupsctl --no-share-printers |
| Content Caching | sudo AssetCacheManagerUtil activate | sudo 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
| Column | Meaning |
|---|---|
| Loss% | Packet loss percentage |
| Snt | Packets sent |
| Last | Last probe time |
| Avg | Average latency |
| Best | Best (lowest) latency |
| Wrst | Worst (highest) latency |
| StDev | Standard 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
| Tool | Purpose | Common Usage |
|---|---|---|
ping | Test connectivity | ping -c 5 host |
traceroute | Trace packet path | traceroute host |
mtr | Real-time trace | mtr -r host |
nettop | Live network monitor | nettop -m tcp |
tcpdump | Packet capture | sudo tcpdump -i en0 |
netstat | Connection stats | netstat -an |
lsof -i | Network by process | lsof -i :port |
networkQuality | Speed test | networkQuality -s |
dig | DNS lookup | dig domain |
nc | Test port | nc -zv host port |
Key troubleshooting steps:
- Verify physical/Wi-Fi connection (
ifconfig) - Check IP configuration (
ipconfig,ifconfig) - Test gateway connectivity (
ping gateway) - Test external connectivity (
ping 8.8.8.8) - Test DNS resolution (
dig,nslookup) - Trace the path (
traceroute,mtr) - 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
| Tool | Purpose | Location |
|---|---|---|
networksetup | High-level Wi-Fi configuration | /usr/sbin/networksetup |
airport | Low-level Wi-Fi diagnostics | Hidden in framework |
wdutil | Wireless diagnostics | /usr/bin/wdutil |
system_profiler | Hardware 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
| Field | Description |
|---|---|
agrCtlRSSI | Signal strength in dBm (higher is better, -50 is excellent, -80 is poor) |
agrCtlNoise | Noise floor in dBm (lower is better) |
state | Connection state (running, init, etc.) |
lastTxRate | Current transmit rate in Mbps |
maxRate | Maximum possible rate |
link auth | Authentication type (wpa2-psk, wpa3, etc.) |
BSSID | MAC address of the access point |
SSID | Network name |
channel | Channel number (and width for 802.11ac/ax) |
NSS | Number 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) | Quality | Typical Use |
|---|---|---|
| -30 to -50 | Excellent | Any application |
| -50 to -60 | Good | Reliable for most uses |
| -60 to -70 | Fair | Web browsing, email |
| -70 to -80 | Weak | May have issues |
| -80 to -90 | Very Weak | Likely 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
| Column | Description |
|---|---|
| SSID | Network name |
| BSSID | Access point MAC address |
| RSSI | Signal strength in dBm |
| CHANNEL | Channel (with width indicator for AC/AX) |
| HT | High Throughput (802.11n+) capable |
| CC | Country code |
| SECURITY | Security protocol |
Channel notation:
6- Channel 6, 20MHz width149,+1- Channel 149, 40MHz bonded with upper channel36,-1- Channel 36, 40MHz bonded with lower channel149,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
| Task | Command |
|---|---|
| Turn Wi-Fi on/off | networksetup -setairportpower en0 on/off |
| Current connection info | airport -I |
| Scan for networks | airport -s |
| Connect to network | networksetup -setairportnetwork en0 "SSID" "password" |
| Disconnect | sudo airport -z |
| Current network | networksetup -getairportnetwork en0 |
| List preferred networks | networksetup -listpreferredwirelessnetworks en0 |
| Remove preferred network | networksetup -removepreferredwirelessnetwork en0 "SSID" |
| Check signal strength | airport -I | grep agrCtlRSSI |
| Show supported channels | airport -c |
| Wi-Fi hardware info | system_profiler SPAirPortDataType |
Key points:
airportis the hidden power tool for Wi-Fi diagnosticsnetworksetuphandles 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:
| Component | Purpose |
|---|---|
| Code Signing | Cryptographically verifies software identity |
| Gatekeeper | Enforces signature requirements for app execution |
| Notarization | Apple’s automated security check for distributed software |
| Hardened Runtime | Restricts dangerous operations in signed code |
Privacy and Sandboxing
Applications are isolated and must request permission for sensitive operations:
| Component | Purpose |
|---|---|
| App Sandbox | Restricts app file system and resource access |
| TCC (Transparency, Consent, Control) | Manages privacy permissions |
| Entitlements | Declares capabilities an app needs |
Hardware-Backed Security
Modern Macs include dedicated security hardware:
| Component | Purpose |
|---|---|
| Secure Enclave | Isolated processor for cryptographic operations |
| T2 / Apple Silicon | Secure boot, encryption keys, biometrics |
| Touch ID | Biometric 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:
| Feature | Intel (T2) | Intel (No T2) | Apple Silicon |
|---|---|---|---|
| Secure Boot | Yes | No | Yes |
| Hardware Encryption | Yes | Software only | Yes |
| Secure Enclave | Yes | No | Yes |
| Boot Security Modes | Limited | No | Full |
| Kernel Extension Loading | Restricted | Allowed | Very 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
- Open System Preferences > Privacy & Security > Full Disk Access
- Click the lock to make changes
- Add Terminal.app (or your terminal emulator)
- 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:
- The signature matches the code content
- The signing certificate is valid
- The certificate chains to a trusted root
- 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"
Kernel Extension Consent
# 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 Type | Purpose | Distribution |
|---|---|---|
| Apple Development | Testing on your devices | Not distributable |
| Apple Distribution | App Store submission | App Store only |
| Developer ID Application | Direct distribution | Outside App Store |
| Developer ID Installer | Signed packages | Outside 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:
| Tool | Purpose |
|---|---|
codesign | Sign and verify code signatures |
spctl | Manage Gatekeeper policies |
xattr | View/modify quarantine attributes |
security find-identity | List 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
- Signed with Developer ID certificate (not Apple Development)
- Hardened runtime enabled (
--options runtime) - Secure timestamp included (
--timestamp) - 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
| Issue | Solution |
|---|---|
| No hardened runtime | Add --options runtime to codesign |
| Missing timestamp | Add --timestamp to codesign |
| Unsigned nested code | Sign all frameworks and helpers |
| Invalid signature | Re-sign with valid Developer ID |
| Forbidden entitlements | Remove or get Apple approval |
| Insecure library loading | Use @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:
| Component | Purpose |
|---|---|
notarytool submit | Upload software for scanning |
notarytool info | Check submission status |
notarytool log | Get detailed scan results |
stapler staple | Attach ticket to software |
spctl -a -vvv | Verify 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:
| Profile | Description |
|---|---|
no-internet | Blocks all network access |
no-network | Blocks network access |
no-write | Blocks file writes |
no-write-except-temporary | Allows writes only to temp |
pure-computation | Minimal 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
| Entitlement | Access Granted |
|---|---|
files.user-selected.read-only | Read files user opens |
files.user-selected.read-write | Read/write user-opened files |
files.downloads.read-only | Read ~/Downloads |
files.downloads.read-write | Read/write ~/Downloads |
files.pictures.read-only | Read ~/Pictures |
files.pictures.read-write | Read/write ~/Pictures |
files.music.read-only | Read ~/Music |
files.music.read-write | Read/write ~/Music |
files.movies.read-only | Read ~/Movies |
files.movies.read-write | Read/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
Container Symlinks
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:
| Component | Purpose |
|---|---|
| App Sandbox | Isolates Mac App Store apps |
| Containers | Per-app data directories |
| Entitlements | Declare required capabilities |
sandbox-exec | Run commands with restrictions |
| Sandbox profiles | Define 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
-
Lock keychain when not in use
$ security lock-keychain -
Set auto-lock timeout
$ security set-keychain-settings -t 300 ~/Library/Keychains/login.keychain-db -
Use specific application ACLs
$ security add-generic-password -T /path/to/specific/app -a user -s service -w pass -
Never put passwords directly in scripts
# Bad PASSWORD="hardcoded" # Good PASSWORD=$(security find-generic-password -s "myservice" -w) -
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:
| Task | Command |
|---|---|
| List keychains | security list-keychains |
| Unlock keychain | security unlock-keychain |
| Find password | security find-generic-password -s "service" -w |
| Add password | security add-generic-password -s "service" -a "user" -w "pass" |
| List certificates | security find-certificate -a |
| Import certificate | security import cert.p12 -k keychain |
| List identities | security 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:
- At the login window, enter wrong password three times
- Option to use recovery key appears
- 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:
- Boot to Recovery Mode
- Open Terminal
- Test unlock with recovery key
- Lock volume again
- Boot normally
Summary
FileVault provides essential full-disk encryption for macOS:
| Command | Purpose |
|---|---|
fdesetup status | Check FileVault status |
fdesetup enable | Enable FileVault |
fdesetup disable | Disable FileVault |
fdesetup list | List enabled users |
fdesetup add | Add FileVault user |
fdesetup remove | Remove FileVault user |
fdesetup changerecovery | Change 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:
| Service | Description |
|---|---|
| Camera | Access to camera device |
| Microphone | Access to microphone |
| Screen Recording | Capture screen content |
| Accessibility | Control UI of other apps |
| Full Disk Access | Access all files |
| Contacts | Address book data |
| Calendar | Calendar events |
| Reminders | Reminders data |
| Photos | Photo library access |
| Location Services | Geographic location |
| System Events | Apple Events automation |
| Files and Folders | Specific folder access |
| Automation | Control other applications |
| Input Monitoring | Monitor keyboard/mouse |
| Media & Apple Music | Apple Music library |
| Developer Tools | Debugging 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:
- Open System Preferences > Security & Privacy > Privacy
- Select “Full Disk Access”
- Click the lock to make changes
- Add Terminal.app (or your terminal emulator)
- 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
- Regularly audit TCC permissions
- Remove unused application permissions
- Be cautious granting Accessibility access
- Use MDM to control permissions in enterprise
Summary
TCC manages privacy permissions on macOS:
| Command | Purpose |
|---|---|
tccutil reset | Reset permissions |
sqlite3 TCC.db | Query permission database |
| System Preferences | GUI permission management |
| MDM Profiles | Enterprise 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
| Feature | Description |
|---|---|
| Secure Boot | Verifies boot process integrity |
| Encrypted Storage | Hardware encryption keys |
| Touch ID | Biometric authentication |
| Secure Enclave | Isolated security processor |
| Image Signal Processor | Camera processing |
| Audio Controller | Microphone 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
| Feature | Description |
|---|---|
| Secure Enclave | Isolated security coprocessor |
| Boot ROM | Hardware root of trust |
| Secure Boot | Cryptographic verification |
| Pointer Authentication | Code integrity protection |
| Kernel Integrity Protection | Runtime kernel verification |
| Device Isolation | Hardware 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
| Level | Description | Use Case |
|---|---|---|
| Full Security | Only Apple-signed software | Default, recommended |
| Reduced Security | Allows notarized kexts | Third-party kexts |
| Permissive Security | No 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
- Restart Mac
- Hold Command + R during startup
- Select Utilities > Startup Security Utility
- Authenticate with admin credentials
Accessing on Apple Silicon Macs
- Shut down Mac completely
- Press and hold Power button until “Loading startup options”
- Click Options > Continue
- 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:
- Recovery Mode requires authentication with a user account
- Activation Lock (tied to Apple ID) provides additional protection
- 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):
| Keys | Function |
|---|---|
| Option (Alt) | Startup Manager |
| Command + R | Recovery Mode |
| Command + Option + R | Internet Recovery |
| Shift | Safe Mode |
| D | Apple Diagnostics |
| N | Network Startup |
| T | Target Disk Mode (Intel) |
| Command + V | Verbose 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:
| Component | T2 Macs | Apple Silicon |
|---|---|---|
| Security Chip | T2 | Integrated |
| Secure Boot | Yes | Yes |
| Boot ROM | T2 | Main chip |
| Firmware Password | Yes | Recovery auth |
| Security Modes | Full/Medium/No | Full/Reduced/Permissive |
| External Boot Control | Yes | Yes |
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
| Tool | Purpose | Example |
|---|---|---|
codesign | Verify code signatures | codesign -v /Applications/App.app |
spctl | Gatekeeper control | spctl --assess -v /path/to/app |
csrutil | SIP management | csrutil status |
fdesetup | FileVault management | fdesetup status |
security | Keychain management | security find-generic-password |
log | System log access | log show --predicate 'subsystem == "com.apple.securityd"' |
tccutil | TCC permission reset | tccutil reset Camera |
xattr | Extended attributes | xattr -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
- Enable FileVault - Encrypt your disk
- Keep SIP enabled - Don’t disable without good reason
- Enable Gatekeeper - Block unsigned apps
- Turn on Firewall - Control network access
- Disable auto-login - Require authentication
- Enable automatic updates - Stay patched
- Use strong passwords - With a password manager
- 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
| Issue | macOS | Linux | Solution |
|---|---|---|---|
| Case sensitivity | Insensitive | Sensitive | Test on Linux or case-sensitive volume |
| Path separator | / | / | Same |
| Home directory | /Users/name | /home/name | Use $HOME or ~ |
| Temp directory | /tmp (→ /private/tmp) | /tmp | Use $TMPDIR or /tmp |
| Extended attrs | Yes (complex) | Yes (simpler) | Strip with xattr -rc before sharing |
Shell Commands
| Task | macOS (BSD) | Linux (GNU) | Portable |
|---|---|---|---|
| List files | ls | ls | Same (basic flags) |
| Extended list | ls -l@ | ls -l | Check for attrs separately |
| sed in-place | sed -i '' | sed -i | sed -i.bak |
| Date format | BSD date | GNU date | Use date +%FORMAT |
| Find with delete | find -delete | find -delete | Same |
| readlink | BSD (limited) | GNU (full) | Install GNU coreutils |
| stat format | stat -f | stat -c | Use format flags carefully |
Network Services
| Service | macOS | Linux | Notes |
|---|---|---|---|
| SSH | OpenSSH | OpenSSH | Mostly identical |
| VNC | Screen Sharing | TigerVNC/x11vnc | Different implementations |
| SMB | Built-in | Samba | macOS client works with Linux servers |
| NFS | Built-in | Built-in | Some option differences |
| mDNS/Bonjour | Built-in | Avahi | Compatible 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?
| Filesystem | macOS | Linux | Windows | Max File Size |
|---|---|---|---|---|
| HFS+ | R/W | Read* | No | 8 EB |
| APFS | R/W | Read* | No | 8 EB |
| NTFS | Read | R/W | R/W | 16 TB |
| FAT32 | R/W | R/W | R/W | 4 GB |
| ExFAT | R/W | R/W | R/W | 16 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
Recommended: tar.gz Without macOS Metadata
# 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
| Issue | Detection | Solution |
|---|---|---|
| Case sensitivity | find . | sort -f | uniq -di | Use case-sensitive volume or rename files |
| Extended attributes | ls -l@, xattr -l | xattr -rc before sharing |
| AppleDouble files | find . -name "._*" | COPYFILE_DISABLE=1 or dot_clean |
| Line endings | file command, cat -v | dos2unix or configure .gitattributes |
| External drives | N/A | Format as ExFAT for universal compatibility |
Key practices:
- Clean files before sharing:
xattr -rc folder/ - Use clean archive creation:
COPYFILE_DISABLE=1 tar czf - Configure Git properly:
.gitattributeswith explicit line ending rules - Test on target platform: Especially for case sensitivity issues
- 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
| Feature | Docker Desktop | Colima | Podman | Lima |
|---|---|---|---|---|
| Setup Complexity | Easy | Easy | Medium | Medium |
| Resource Usage | High | Low | Medium | Low |
| Docker CLI | Native | Native | Compatible | Manual |
| GUI | Yes | No | Optional | No |
| Kubernetes | Built-in | Add-on | Optional | Templates |
| License | Commercial* | Free | Free | Free |
| File Performance | Good | Good | Adequate | Good |
| Apple Silicon | Yes | Yes | Yes | Yes |
| x86 Emulation | Yes | Via Rosetta | Via QEMU | Via 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:
- Containers on macOS always run in a Linux VM
- Apple’s Virtualization.framework provides near-native performance
- File sharing between macOS and containers has performance overhead
- VirtioFS significantly improves file sharing performance
- 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
-
Download an ISO: Get a Linux distribution ISO (e.g., Ubuntu, Fedora)
-
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
-
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
| Mode | Use Case | Performance |
|---|---|---|
| Virtualize | ARM Linux on Apple Silicon | Near-native |
| Virtualize | x86 Linux on Intel Mac | Near-native |
| Emulate | x86 Linux on Apple Silicon | Slow (~10% native) |
| Emulate | ARM Linux on Intel Mac | Slow |
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:
- File → New
- Download or select Linux ISO
- Configure resources
- 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
| Solution | License | Best For | Apple Silicon |
|---|---|---|---|
| UTM | Free | Personal use, testing | Good (QEMU) |
| Parallels | Commercial | Developers, Windows users | Excellent |
| VMware Fusion | Commercial/Free* | Enterprise, existing VMware | Good |
| Virtualization.framework | N/A (API) | Custom solutions, Lima/Tart | Native |
*Free personal use license available
Key considerations:
- Apple Silicon: Native ARM VMs are fast; x86 requires emulation or Rosetta
- File sharing: VirtioFS is fastest, SSHFS is most flexible
- Networking: NAT for isolation, bridged for network testing
- Snapshots: Essential for development and testing workflows
- 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
}
readlink: Getting Real Paths
# 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
| Challenge | Portable Solution |
|---|---|
| Shebang | #!/usr/bin/env bash |
| OS detection | uname -s + case statement |
| sed in-place | sed -i.bak + rm, or wrapper function |
| date formatting | OS-specific wrapper function |
| readlink -f | Pure bash function or Python fallback |
| stat format | OS-specific wrapper function |
| Script directory | BASH_SOURCE[0] with symlink resolution |
| Colors | Check [[ -t 1 ]] and $TERM |
| Downloads | Check for curl/wget/fetch |
| Temp files | mktemp with fallback |
Key practices:
- Use
#!/usr/bin/env bashfor bash scripts - Detect OS early and branch for platform-specific code
- Create wrapper functions for incompatible commands
- Test with ShellCheck and on multiple platforms
- Use POSIX sh for maximum portability when bash features aren’t needed
- 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
| Type | Security | Compatibility | Recommendation |
|---|---|---|---|
| Ed25519 | Excellent | Modern systems | Recommended |
| RSA 4096 | Very good | Universal | Legacy/compatibility |
| ECDSA | Good | Wide | Alternative |
| DSA | Deprecated | Old systems | Avoid |
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
| Option | Description | Example |
|---|---|---|
| HostName | Real hostname/IP | HostName 192.168.1.100 |
| User | Login username | User admin |
| Port | SSH port | Port 2222 |
| IdentityFile | Path to private key | IdentityFile ~/.ssh/key |
| IdentitiesOnly | Only use specified keys | IdentitiesOnly yes |
| ForwardAgent | Forward ssh-agent | ForwardAgent yes |
| ProxyJump | Jump through host | ProxyJump bastion |
| LocalForward | Forward local port | LocalForward 8080 localhost:80 |
| RemoteForward | Forward remote port | RemoteForward 9090 localhost:9090 |
| DynamicForward | SOCKS proxy | DynamicForward 1080 |
| ServerAliveInterval | Keep-alive interval | ServerAliveInterval 60 |
| Compression | Enable compression | Compression yes |
| StrictHostKeyChecking | Host key checking | StrictHostKeyChecking 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:
- First connection prompts for passphrase
- Passphrase is stored in macOS Keychain
- 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
| Option | Description |
|---|---|
| ControlMaster auto | Create master if none exists |
| ControlMaster yes | Always create master |
| ControlMaster no | Don’t use multiplexing |
| ControlPersist 600 | Keep master for 600 seconds |
| ControlPersist yes | Keep 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
| Task | Command/Config |
|---|---|
| Generate key | ssh-keygen -t ed25519 |
| Add to agent | ssh-add ~/.ssh/key |
| Copy key | ssh-copy-id user@host |
| Quick connect | Configure in ~/.ssh/config |
| Jump host | ProxyJump bastion |
| Port forward | LocalForward 5432 localhost:5432 |
| Multiplexing | ControlMaster auto |
| Debug | ssh -vvv user@host |
Key practices:
- Use Ed25519 keys with strong passphrases
- Configure
~/.ssh/configfor all regular hosts - Use SSH agent with Keychain integration
- Enable multiplexing for faster connections
- Use ProxyJump for bastion access
- Keep separate keys for different purposes
- 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:
- System Settings → General → Sharing
- Enable “Screen Sharing”
- 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
| Method | Enable Command |
|---|---|
| SSH | sudo systemsetup -setremotelogin on |
| Screen Sharing | System Settings → Sharing → Screen Sharing |
| Remote Management | kickstart -activate -configure -allowAccessFor -allUsers -privs -all |
Connecting
| From | To | Method |
|---|---|---|
| macOS | macOS | open vnc://host.local |
| macOS | Linux | VNC client + SSH tunnel |
| macOS | Windows | Microsoft Remote Desktop |
| Any | macOS (internet) | SSH tunnel + VNC |
Ports
| Service | Port |
|---|---|
| VNC | 5900 (display :0), 5901 (:1), etc. |
| ARD | 3283 |
| RDP | 3389 |
| SSH | 22 |
Summary
Remote access options depend on your use case:
| Use Case | Recommended Approach |
|---|---|
| Mac to Mac (local) | Built-in Screen Sharing |
| Mac to Mac (internet) | SSH tunnel + Screen Sharing |
| Mac to Linux | SSH + VNC through tunnel |
| Mac to Windows | Microsoft Remote Desktop |
| Headless Mac admin | SSH + Remote Management |
| Run Linux GUI apps | SSH with X11 forwarding |
| Easy internet access | Tailscale + Screen Sharing |
Key security practices:
- Never expose VNC directly to the internet
- Always use SSH tunnels for remote VNC
- Consider VPN solutions like Tailscale for easier secure access
- Use strong authentication
- 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
| Option | Description |
|---|---|
-ro | Read-only export |
-rw | Read-write (rarely needed, it’s default) |
-alldirs | Allow mounting any subdirectory |
-maproot=user | Map root to specified user |
-mapall=user | Map all users to specified user |
-network | Allowed network |
-mask | Network 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
| Protocol | Command |
|---|---|
| SMB | mount_smbfs //user@server/share /mnt |
| NFS | sudo mount -t nfs -o resvport server:/share /mnt |
| Finder | open smb://server/share or open nfs://server/share |
Common Mount Options
| Protocol | Option | Description |
|---|---|---|
| NFS | resvport | Use privileged port (required for most servers) |
| NFS | vers=3 or vers=4 | NFS version |
| NFS | soft | Allow operations to fail |
| SMB | nobrowse | Hide from Finder sidebar |
| Both | ro | Read-only |
Configuration Files
| File | Purpose |
|---|---|
/etc/nsmb.conf | SMB client configuration |
/etc/exports | NFS server exports |
/etc/auto_master | Autofs master map |
/etc/auto_* | Autofs mount maps |
/etc/fstab | Static mounts (limited use on macOS) |
Summary
Choosing between NFS and SMB:
| Use Case | Recommended |
|---|---|
| Mac ↔ Mac | SMB (default) or NFS |
| Mac ↔ Linux | NFS (if pure Unix) or SMB (if mixed) |
| Mac ↔ Windows | SMB |
| Mixed environment | SMB (most compatible) |
| Performance critical (Unix) | NFS |
| Simple setup | SMB (Finder support) |
Key practices:
- Use autofs for on-demand mounting
- Store SMB credentials in Keychain
- Use
resvportoption for NFS on macOS - Configure user mapping for NFS to avoid ownership issues
- Tune SMB settings in
/etc/nsmb.conffor performance - 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:
| Concept | Description |
|---|---|
| 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 Throttling | Thermal 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:
| Feature | Purpose |
|---|---|
| Unified Memory | Shared CPU/GPU memory (Apple Silicon) |
| Memory Compression | Compress inactive pages instead of swapping |
| App Nap | Reduce memory/CPU for background apps |
| Memory Pressure | System-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:
| Factor | Impact |
|---|---|
| SSD Trim | Maintains write performance |
| APFS Snapshots | Can consume space |
| Spotlight Indexing | Background I/O during indexing |
| Time Machine | Periodic 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:
| Aspect | Consideration |
|---|---|
| CPU Frequency | Dynamic scaling based on demand |
| Display Brightness | Major power consumer |
| Discrete GPU | Significant power draw when active |
| Background Activity | Apps 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:
| Task | GUI Tool | CLI Tool |
|---|---|---|
| Process monitoring | Activity Monitor | top, htop, ps |
| Memory analysis | Activity Monitor | vm_stat, memory_pressure |
| Disk I/O | Activity Monitor | iostat, fs_usage |
| Network | Activity Monitor | nettop, netstat |
| CPU profiling | Instruments | sample, spindump |
| Energy | Activity Monitor | powermetrics |
| System trace | Instruments | fs_usage, sc_usage |
Performance on Apple Silicon vs Intel
Apple Silicon Macs have fundamentally different performance characteristics:
| Aspect | Intel Mac | Apple Silicon Mac |
|---|---|---|
| Memory | Dedicated RAM | Unified Memory (shared CPU/GPU) |
| Cores | Symmetric | Asymmetric (P-cores + E-cores) |
| Power States | Intel SpeedStep | Apple custom (more granular) |
| Thermal Design | Often throttles under load | More consistent performance |
| GPU | Discrete or integrated | Integrated, shares memory |
| Rosetta 2 | N/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
- Monitor before optimizing: Establish baseline measurements
- Focus on bottlenecks: Optimize the limiting factor first
- Consider power impact: Performance gains may cost battery life
- Test on target hardware: Performance varies by Mac model
- 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
| Column | Description |
|---|---|
| Process Name | Application or process name |
| % CPU | Percentage of CPU time used |
| CPU Time | Total CPU time consumed |
| Threads | Number of active threads |
| Idle Wake Ups | Times process woke from idle |
| PID | Process identifier |
| User | Username owning the process |
Hidden CPU Columns
Right-click the column header to add these valuable hidden columns:
| Column | Description | When Useful |
|---|---|---|
| % GPU | GPU utilization | Graphics/video work |
| GPU Time | Total GPU time | Identifying GPU hogs |
| Architecture | arm64/x86_64 | Finding Rosetta processes |
| Sandbox | Sandboxed status | Security analysis |
| Restricted | Hardened runtime | Security analysis |
| App Nap | App Nap status | Energy debugging |
| Sudden Termination | Can be killed safely | Shutdown debugging |
| Preventing Sleep | Blocking system sleep | Battery 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
| Column | Description |
|---|---|
| Memory | Current memory footprint |
| Real Memory | Physical RAM used |
| Virtual Memory | Address space size |
| Shared Memory | Memory shared with other processes |
| Real Private Memory | Non-shared physical RAM |
| Compressed Memory | Compressed pages in RAM |
Hidden Memory Columns
| Column | Description | When Useful |
|---|---|---|
| Purgeable Memory | Memory that can be reclaimed | Memory optimization |
| Real Shared Memory | Actually shared RAM | Library sharing analysis |
| Dirty Memory | Modified pages | Swap prediction |
| Swapped Memory | Memory paged to disk | Performance 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:
| Color | Meaning | Action |
|---|---|---|
| Green | Memory available | Normal operation |
| Yellow | Memory becoming limited | Consider closing apps |
| Red | Memory critically low | Close apps, investigate |
Memory Categories:
| Category | Description |
|---|---|
| App Memory | Memory actively used by applications |
| Wired Memory | Kernel memory, cannot be compressed/swapped |
| Compressed | Inactive pages compressed in RAM |
| Cached Files | File 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
| Column | Description |
|---|---|
| Energy Impact | Relative power consumption |
| Avg Energy Impact | Average over last 8 hours |
| App Nap | Is App Nap active |
| Preventing Sleep | Blocking system sleep |
| Graphics Card | Which GPU in use |
Hidden Energy Columns
| Column | Description | When Useful |
|---|---|---|
| Power | Instantaneous power draw | Battery debugging |
| Requires High Perf GPU | Needs discrete GPU | GPU switching |
| GPU Activity | GPU busy percentage | Graphics 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:
| Score | Impact | Example |
|---|---|---|
| 0-4 | Low | Text editors, system utilities |
| 4-12 | Medium | Browsers (idle), communication apps |
| 12-30 | High | Video playback, compilation |
| 30+ | Very High | Gaming, 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
| Column | Description |
|---|---|
| Bytes Read | Total bytes read from disk |
| Bytes Written | Total bytes written to disk |
| Reads In | Number of read operations |
| Writes Out | Number of write operations |
Hidden Disk Columns
| Column | Description | When Useful |
|---|---|---|
| Bytes Read/sec | Read throughput | I/O bottleneck detection |
| Bytes Written/sec | Write throughput | I/O bottleneck detection |
| Read Delta | Recent reads | Active I/O identification |
| Write Delta | Recent writes | Active 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
| Column | Description |
|---|---|
| Sent Bytes | Total bytes transmitted |
| Received Bytes | Total bytes received |
| Sent Packets | Number of packets sent |
| Received Packets | Number of packets received |
Hidden Network Columns
| Column | Description | When Useful |
|---|---|---|
| Sent Bytes/sec | Upload rate | Bandwidth monitoring |
| Received Bytes/sec | Download rate | Bandwidth monitoring |
| Sent Packets/sec | Packet rate | Network debugging |
| Received Packets/sec | Packet rate | Network 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:
- View menu > All Processes
- 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:
sysdiagnosein the background- Creates a comprehensive system report
- Saves to
~/Desktopor specified location
# CLI equivalent
$ sudo sysdiagnose
Spindump
When an app is unresponsive:
- Select the process
- View > Sample Process or View > Spindump
# CLI equivalent
$ sudo spindump PID 5 -file /tmp/spindump.txt
Exporting Data
Copy Process Information
- Select process(es)
- 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
| Feature | Activity Monitor | CLI Tools |
|---|---|---|
| Visual graphs | Yes | No (unless using htop) |
| Process hierarchy | Yes | pstree, ps -ef |
| Real-time updates | Yes | top, htop |
| Sample process | Yes | sample command |
| Export data | Copy only | Redirect to file |
| Scriptable | Limited | Fully scriptable |
| Remote access | No | Via SSH |
| Resource usage | Higher | Lower |
Best Practices
For Troubleshooting
- Start with the right tab: CPU for slow system, Memory for app crashes
- Enable hidden columns: Architecture, Preventing Sleep are invaluable
- Use hierarchical view: Find parent processes causing issues
- Sample unresponsive apps: Gather data before force quitting
For Monitoring
- Set Dock icon to CPU: Quick visual indicator
- Keep Activity Monitor running: Catch intermittent issues
- Check Memory Pressure regularly: Early warning of problems
- Review Energy tab on battery: Identify power hogs
For Analysis
- Sort by relevant metric: % CPU for performance, Energy for battery
- Watch over time: Patterns reveal issues better than snapshots
- Cross-reference tabs: High memory often correlates with disk I/O
- Use Inspect window: Deep dive into suspicious processes
Summary
Activity Monitor provides comprehensive system monitoring:
| Tab | Key Metrics | Watch For |
|---|---|---|
| CPU | % CPU, System/User split | Runaway processes, high system % |
| Memory | Memory Pressure, Compressed | Yellow/red pressure, swap usage |
| Energy | Energy Impact, Preventing Sleep | Battery drainers, sleep blockers |
| Disk | Read/Write rates | Excessive I/O, constant writes |
| Network | Send/Receive rates | Unexpected 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:
| Key | Action |
|---|---|
q | Quit |
o | Change sort order |
O | Secondary sort |
s | Change update interval |
U | Filter by user |
S | Toggle cumulative mode |
p | Toggle process ID |
e | Toggle 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:
| Section | Meaning |
|---|---|
| Load Avg | 1, 5, 15-minute CPU load averages |
| CPU usage | User/system/idle breakdown |
| PhysMem | Physical memory: used, wired, compressed, unused |
| VM | Virtual memory and swap activity |
| Networks | Network I/O summary |
| Disks | Disk I/O summary |
Process columns:
| Column | Meaning |
|---|---|
| %CPU | CPU utilization percentage |
| TIME | Total CPU time consumed |
| #TH | Thread count |
| #WQ | Work queue threads |
| #PORT | Mach ports |
| MEM | Memory footprint |
| PURG | Purgeable memory |
| CMPRS | Compressed memory |
| PGRP | Process 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
| Feature | top | htop |
|---|---|---|
| Colors | No | Yes |
| Mouse support | No | Yes |
| Scroll process list | Limited | Yes |
| Tree view | No | Yes |
| Kill with key | No | Yes |
| Search | No | Yes |
| Filter | Limited | Yes |
| Setup menu | No | Yes |
Interactive Commands
| Key | Action |
|---|---|
F1 / h | Help |
F2 / S | Setup menu |
F3 / / | Search |
F4 / \ | Filter |
F5 / t | Tree view |
F6 / > | Sort by column |
F9 / k | Kill process |
F10 / q | Quit |
Space | Tag process |
u | Filter by user |
p | Sort by CPU |
M | Sort by memory |
T | Sort 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:
| Metric | Meaning |
|---|---|
| Pages free | Available memory pages |
| Pages active | Recently used pages |
| Pages inactive | Not recently used, can be reclaimed |
| Pages wired down | Kernel memory, cannot be paged out |
| Pages stored in compressor | Compressed memory |
| Pageins | Pages read from disk |
| Pageouts | Pages written to disk (swap) |
| Swapins/Swapouts | Swap 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:
| Level | Meaning |
|---|---|
| 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
| Column | Meaning |
|---|---|
| us | User CPU % |
| sy | System CPU % |
| id | Idle CPU % |
| KB/t | Kilobytes per transfer |
| tps | Transfers per second |
| MB/s | Megabytes 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
| Column | Meaning |
|---|---|
| Timestamp | When the call occurred |
| System call | Type of operation |
| Path/Details | File path or file descriptor |
| Duration | Time for the operation |
| Process | Process 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
| Tool | Purpose | Root Required | Continuous |
|---|---|---|---|
| top | Process monitor | No | Yes |
| htop | Enhanced process monitor | Optional | Yes |
| vm_stat | Memory statistics | No | Yes |
| memory_pressure | Memory pressure | No | No |
| iostat | I/O statistics | No | Yes |
| fs_usage | File system trace | Yes | Yes |
| powermetrics | Power analysis | Yes | Yes |
| sample | Process profiling | No | One-shot |
| spindump | System sampling | Yes | One-shot |
| nettop | Network by process | No | Yes |
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 Class | Description | Core Preference |
|---|---|---|
| User Interactive | UI animations, event handling | P-cores |
| User Initiated | User-requested tasks | P-cores |
| Default | Normal priority | P-cores or E-cores |
| Utility | Long-running tasks | E-cores preferred |
| Background | Non-visible work | E-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:
- Find the app in Finder
- Right-click > Get Info
- 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 Type | Native Performance | Rosetta Performance |
|---|---|---|
| CPU-bound | 100% | ~70-90% |
| Memory-bound | 100% | ~80-95% |
| I/O-bound | 100% | ~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:
| Feature | Status |
|---|---|
| AVX/AVX2/AVX-512 | Not supported |
| Kernel extensions | Not supported |
| Virtualization (hypervisor) | Not supported |
| Some Intel-specific instructions | May 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
| Aspect | Impact |
|---|---|
| GPU memory | Limited only by total RAM |
| Data transfer | Zero-copy CPU/GPU sharing |
| Memory pressure | Affects both CPU and GPU |
| Upgrade | Not 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
| Factor | Intel Advantage | Apple Silicon Advantage |
|---|---|---|
| Single-thread perf | Similar or lower | Generally higher |
| Multi-thread perf | Higher core counts (desktop) | Better power efficiency |
| Power efficiency | Lower | Significantly higher |
| Thermal throttling | More common | Less common |
| Boot time | 30-60 seconds | 10-15 seconds |
| Wake from sleep | 1-3 seconds | Instant |
| x86 software | Native | ~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:
| Architecture | Homebrew Prefix | Shell Path |
|---|---|---|
| Intel | /usr/local | Added automatically |
| Apple Silicon | /opt/homebrew | Requires 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 Type | Intel Mac | Apple Silicon Mac |
|---|---|---|
| macOS guest | Intel macOS | ARM macOS only |
| Windows guest | x86/x64 Windows | ARM Windows only |
| Linux guest | x86/x64 Linux | ARM Linux (x86 via emulation) |
| Virtualization.framework | No | Yes |
# 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
- Use native apps when possible (check Architecture in Activity Monitor)
- Enable automatic graphics switching for battery life
- Leverage QoS in your code for proper core scheduling
- Use Metal for graphics and compute workloads
- Profile on target hardware - performance varies by chip
For Intel Macs
- Monitor thermal throttling with
powermetrics - Manage discrete GPU usage for battery life
- Check for AVX support in performance-critical code
- 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.
...
| Category | Description | Can Be Reclaimed |
|---|---|---|
| Free | Immediately available | N/A |
| Active | Recently used by apps | Yes, if needed |
| Inactive | Not recently used | Yes, readily |
| Speculative | Preemptively cached files | Yes, readily |
| Wired | Kernel, drivers, system | No |
| Purgeable | App-marked disposable | Yes, readily |
| Compressed | Inactive, compressed in RAM | Partially |
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
| Level | State | System Behavior |
|---|---|---|
| 1 | Normal | No special action |
| 2 | Warning | Compress memory, notify apps |
| 4 | Critical | Aggressive 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
| Category | Description |
|---|---|
| VIRTUAL SIZE | Address space allocated |
| RESIDENT SIZE | Actually in RAM |
| DIRTY SIZE | Modified, must be saved |
| SWAPPED OUT | Paged to disk |
| PURGEABLE | Can 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
| Aspect | Traditional | Unified Memory |
|---|---|---|
| GPU allocation | Dedicated VRAM | Shared RAM pool |
| Data transfer | Copy between RAM/VRAM | Zero-copy |
| Total available | RAM + VRAM | Single RAM pool |
| Memory pressure | Separate | Combined |
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:
| Concept | Description | Command |
|---|---|---|
| Memory Pressure | System-wide memory demand | memory_pressure |
| Compression | In-memory page compression | vm_stat | grep compressor |
| Wired Memory | Non-pageable kernel memory | vm_stat | grep wired |
| Swap | Disk-backed virtual memory | sysctl vm.swapusage |
| RSS | Resident Set Size (actual RAM) | ps -o rss |
| VSZ | Virtual 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:
| Metric | Description | Healthy Range |
|---|---|---|
| KB/t | Kilobytes per transfer | Higher = more efficient |
| tps | Transfers per second | Depends on workload |
| MB/s | Throughput | SSD: 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
| Symptom | Possible Cause |
|---|---|
| Beach ball cursor | App waiting on I/O |
| Slow app launch | Disk reading app files |
| System unresponsive | Heavy disk activity |
| High CPU wait | I/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
| Process | Activity | Solution |
|---|---|---|
| mds, mds_stores | Spotlight indexing | Wait, or exclude folders |
| backupd | Time Machine | Schedule backups |
| photolibraryd | Photos library | Wait for completion |
| bird | iCloud sync | Check iCloud status |
| nsurlsessiond | Background downloads | Check 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:
| Tool | Purpose | Usage |
|---|---|---|
| iostat | I/O statistics | iostat -d -w 2 |
| fs_usage | Real-time I/O tracing | sudo fs_usage -f diskio |
| diskutil | Disk management | diskutil apfs list |
| smartctl | SSD health | sudo smartctl -a /dev/disk0 |
| tmutil | Snapshot management | tmutil listlocalsnapshots / |
APFS features for performance:
| Feature | Benefit |
|---|---|
| Space sharing | Efficient multi-volume usage |
| Clones | Instant file copies |
| Snapshots | Fast point-in-time copies |
| Sparse files | Efficient large file handling |
| Copy-on-write | Reduced 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
| Key | Description | Values |
|---|---|---|
| displaysleep | Display sleep timeout (min) | 0 = never, N = minutes |
| disksleep | Disk sleep timeout (min) | 0 = never, N = minutes |
| sleep | System sleep timeout (min) | 0 = never, N = minutes |
| womp | Wake on Magic Packet (LAN) | 0/1 |
| powernap | Power Nap enabled | 0/1 |
| hibernatemode | Hibernate mode | 0, 3, or 25 |
| standby | Standby mode | 0/1 |
| standbydelay | Delay before standby (sec) | seconds |
| autopoweroff | Auto power off | 0/1 |
| lowpowermode | Low Power Mode | 0/1 |
| tcpkeepalive | TCP keepalive during sleep | 0/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:
| Mode | RAM Power | Hibernate File | Wake Time | Battery Use |
|---|---|---|---|---|
| 0 | Always on | No | Instant | Higher |
| 3 | On until standby | Yes | Instant/Slow | Medium |
| 25 | Off | Yes | Slow | Minimal |
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
| Option | Prevents | Use Case |
|---|---|---|
| -d | Display sleep | Presentations |
| -i | Idle sleep | Long-running tasks |
| -m | Disk sleep | Large file operations |
| -s | System sleep | Downloads, backups |
| -u | Declare user active | Simulate user activity |
| -t N | All for N seconds | Time-limited prevention |
| -w PID | While PID runs | Follow 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
| Assertion | Effect |
|---|---|
| PreventUserIdleSystemSleep | System won’t idle sleep |
| PreventUserIdleDisplaySleep | Display won’t idle sleep |
| PreventSystemSleep | System cannot sleep at all |
| NoIdleSleepAssertion | Legacy, same as PreventUserIdleSystemSleep |
| NoDisplaySleepAssertion | Legacy, 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:
- View > Columns > App Nap
- “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
- Reduce screen brightness - Major power consumer
- Disable Bluetooth/Wi-Fi when not needed
- Close unused tabs - Browser tabs use memory and CPU
- Enable Low Power Mode - Significant battery extension
- Check Activity Monitor Energy tab - Find specific drains
- Disable Power Nap on battery - Prevents background wake
- 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
| Level | State | Behavior |
|---|---|---|
| 0 | Nominal | Normal operation |
| 1 | Moderate | Slight throttling |
| 2-9 | Elevated | Increasing throttling |
| 10+ | Critical | Aggressive throttling, possible shutdown |
Summary
Key power management commands:
| Command | Purpose | Example |
|---|---|---|
| pmset -g | View settings | pmset -g batt |
| pmset -a | Set for all sources | sudo pmset -a sleep 30 |
| pmset -b | Set for battery | sudo pmset -b lowpowermode 1 |
| pmset -c | Set for AC power | sudo pmset -c sleep 0 |
| caffeinate | Prevent sleep | caffeinate -s make all |
| powermetrics | Power analysis | sudo 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:
- Check all three resource types
- Identify which is the bottleneck
- Investigate the specific resource
- 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
| Issue | Cause | Solution |
|---|---|---|
| kernel_task high CPU | Thermal throttling | Cool the Mac, check vents |
| mds_stores high CPU | Spotlight indexing | Wait, or exclude folders |
| WindowServer high CPU | Graphics issue | Reduce transparency, check GPU |
| nsurlsessiond | Background downloads | Check for updates |
| softwareupdated | System updates | Let it complete |
| Single app 100%+ CPU | App issue | Restart 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
| Issue | Cause | Solution |
|---|---|---|
| Disk nearly full | Insufficient space | Free up space |
| Constant disk activity | Spotlight/Time Machine | Wait or exclude |
| Slow read/write | Disk failing | Run diagnostics |
| Beach ball on save | Disk I/O blocked | Check 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:
- Identify symptoms - What feels slow?
- Check resources - CPU, memory, disk, network
- Find the bottleneck - Which resource is constrained?
- Identify the cause - Which process or condition?
- Apply the fix - Address root cause
- Verify resolution - Confirm improvement
Essential diagnostic commands:
| Resource | Quick Check | Detailed |
|---|---|---|
| CPU | top -l 1 -n 0 | sample PID |
| Memory | memory_pressure | vmmap PID |
| Disk | iostat -d | sudo fs_usage |
| Network | nettop -P | netstat -an |
| Power | pmset -g batt | sudo powermetrics |
| Overall | uptime | sysdiagnose |
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#- Requiressudo(administrator privileges)[optional]- Optional parameter<required>- Required parameter
File and Directory Operations
Navigation and Listing
| Command | Description | Example |
|---|---|---|
pwd | Print working directory | $ pwd |
cd <dir> | Change directory | $ cd ~/Documents |
cd - | Return to previous directory | $ cd - |
ls | List directory contents | $ ls -la |
ls -la | Long format with hidden files | $ ls -la ~/ |
ls -lah | Human-readable sizes | $ ls -lah |
ls -lt | Sort by modification time | $ ls -lt |
tree | Directory tree (install via Homebrew) | $ tree -L 2 |
File Operations
| Command | Description | Example |
|---|---|---|
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
| Command | Description | Example |
|---|---|---|
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 1 | Disk 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
| Command | Description | Example |
|---|---|---|
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 -le | List with ACLs | $ ls -le |
chmod +a <acl> | Add ACL entry | $ chmod +a "user:joe:allow read" file |
Text Processing
Viewing Files
| Command | Description | Example |
|---|---|---|
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
| Command | Description | Example |
|---|---|---|
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
| Command | Description | Example |
|---|---|---|
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 |
uniq | Remove adjacent duplicates | $ sort file | uniq |
uniq -c | Count occurrences | $ sort file | uniq -c |
cut -d: -f1 | Extract field | $ cut -d: -f1 /etc/passwd |
cut -c1-10 | Extract 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
| Command | Description | Example |
|---|---|---|
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
| Command | Description | Example |
|---|---|---|
find <path> -name <pattern> | Find by name | $ find . -name "*.txt" |
find <path> -type f | Find files only | $ find . -type f |
find <path> -type d | Find directories only | $ find /var -type d |
find <path> -mtime -7 | Modified in last 7 days | $ find . -mtime -7 |
find <path> -size +100M | Files larger than 100MB | $ find . -size +100M |
find <path> -empty | Find 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
| Command | Description | Example |
|---|---|---|
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
| Command | Description | Example |
|---|---|---|
ps aux | List all processes | $ ps aux |
ps aux | grep <name> | Find specific process | $ ps aux | grep nginx |
pgrep <name> | Get PIDs by name | $ pgrep -l Safari |
top | Interactive process viewer | $ top |
htop | Enhanced top (install via Homebrew) | $ htop |
Activity Monitor | GUI process manager | Open from Spotlight |
Process Control
| Command | Description | Example |
|---|---|---|
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 & |
jobs | List background jobs | $ jobs |
fg | Bring to foreground | $ fg %1 |
bg | Continue in background | $ bg %1 |
nohup <cmd> & | Run immune to hangups | $ nohup ./script.sh & |
Ctrl+Z | Suspend current process | |
Ctrl+C | Interrupt current process |
Resource Usage
| Command | Description | Example |
|---|---|---|
vm_stat | Virtual memory statistics | $ vm_stat |
iostat | I/O statistics | $ iostat |
fs_usage | Filesystem activity | # fs_usage -f filesys |
sample <PID> <secs> | Sample process | $ sample Safari 5 |
System Information
| Command | Description | Example |
|---|---|---|
uname -a | System information | $ uname -a |
sw_vers | macOS version | $ sw_vers |
system_profiler | Detailed system info | $ system_profiler SPHardwareDataType |
hostname | Show hostname | $ hostname |
whoami | Current username | $ whoami |
id | User ID and groups | $ id |
uptime | System uptime | $ uptime |
date | Current date/time | $ date |
cal | Calendar | $ cal |
df -h | Disk free space | $ df -h |
diskutil list | List disks and partitions | $ diskutil list |
diskutil info <disk> | Disk information | $ diskutil info disk0 |
sysctl -a | All kernel parameters | $ sysctl -a |
sysctl hw.memsize | Total RAM | $ sysctl hw.memsize |
sysctl machdep.cpu | CPU information | $ sysctl machdep.cpu |
ioreg -l | IOKit registry | $ ioreg -l |
nvram -p | NVRAM variables | $ nvram -p |
Network Commands
Network Information
| Command | Description | Example |
|---|---|---|
ifconfig | Network interface config | $ ifconfig |
ifconfig en0 | Specific interface | $ ifconfig en0 |
ipconfig getifaddr en0 | Get IP address | $ ipconfig getifaddr en0 |
networksetup -listallnetworkservices | List services | $ networksetup -listallnetworkservices |
networksetup -getinfo "Wi-Fi" | Service info | $ networksetup -getinfo "Wi-Fi" |
scutil --dns | DNS configuration | $ scutil --dns |
scutil --proxy | Proxy configuration | $ scutil --proxy |
netstat -an | Network connections | $ netstat -an |
netstat -rn | Routing table | $ netstat -rn |
lsof -i | Open network connections | $ lsof -i |
lsof -i :80 | Connections on port 80 | $ lsof -i :80 |
Connectivity Testing
| Command | Description | Example |
|---|---|---|
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 -a | ARP table | $ arp -a |
Wi-Fi
| Command | Description | Example |
|---|---|---|
networksetup -setairportpower en0 on | Turn Wi-Fi on | $ networksetup -setairportpower en0 on |
networksetup -setairportpower en0 off | Turn Wi-Fi off | $ networksetup -setairportpower en0 off |
networksetup -getairportnetwork en0 | Current network | $ networksetup -getairportnetwork en0 |
Disk and Volume Management
| Command | Description | Example |
|---|---|---|
diskutil list | List 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 list | List 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 create | Create disk image | $ hdiutil create -size 1g -fs APFS -volname "MyDisk" disk.dmg |
tmutil | Time Machine utility | $ tmutil listbackups |
User Management
| Command | Description | Example |
|---|---|---|
whoami | Current user | $ whoami |
id | User/group IDs | $ id |
groups | Group memberships | $ groups |
dscl . -list /Users | List all users | $ dscl . -list /Users |
dscl . -list /Groups | List all groups | $ dscl . -list /Groups |
dscl . -read /Users/<user> | User details | $ dscl . -read /Users/admin |
dscacheutil -q user | Query user cache | $ dscacheutil -q user |
sysadminctl -addUser | Add user | # sysadminctl -addUser joe -password pass |
sysadminctl -deleteUser | Delete user | # sysadminctl -deleteUser joe |
sudo -s | Root shell | $ sudo -s |
su - <user> | Switch user | $ su - otheruser |
Service and Process Management
launchctl
| Command | Description | Example |
|---|---|---|
launchctl list | List 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 kickstart | Start service | $ launchctl kickstart gui/501/com.example.agent |
launchctl kill <sig> <service> | Send signal | # launchctl kill SIGTERM system/com.example.daemon |
launchctl print system | Print system domain | $ launchctl print system |
launchctl print gui/501 | Print user domain | $ launchctl print gui/501 |
Homebrew Services
| Command | Description | Example |
|---|---|---|
brew services list | List 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)
| Command | Description | Example |
|---|---|---|
brew install <pkg> | Install package | $ brew install wget |
brew uninstall <pkg> | Uninstall package | $ brew uninstall wget |
brew upgrade | Upgrade all packages | $ brew upgrade |
brew upgrade <pkg> | Upgrade specific package | $ brew upgrade node |
brew update | Update Homebrew | $ brew update |
brew search <term> | Search packages | $ brew search python |
brew info <pkg> | Package information | $ brew info python |
brew list | List installed packages | $ brew list |
brew list --cask | List installed casks | $ brew list --cask |
brew outdated | Show outdated packages | $ brew outdated |
brew cleanup | Remove old versions | $ brew cleanup |
brew doctor | Diagnose 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
| Command | Description | Example |
|---|---|---|
pbcopy | Copy to clipboard | $ cat file.txt | pbcopy |
pbpaste | Paste from clipboard | $ pbpaste > output.txt |
pbcopy < file.txt | Copy file contents | $ pbcopy < ~/.ssh/id_rsa.pub |
Launching and Opening
| Command | Description | Example |
|---|---|---|
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
| Command | Description | Example |
|---|---|---|
say <text> | Text-to-speech | $ say "Hello world" |
screencapture | Take screenshot | $ screencapture screen.png |
screencapture -c | Screenshot to clipboard | $ screencapture -c |
pmset -g | Power management status | $ pmset -g |
pmset -g batt | Battery status | $ pmset -g batt |
caffeinate | Prevent sleep | $ caffeinate -t 3600 |
softwareupdate -l | List software updates | $ softwareupdate -l |
softwareupdate -ia | Install all updates | # softwareupdate -ia |
defaults read | Read preferences | $ defaults read com.apple.finder |
defaults write | Write preferences | $ defaults write com.apple.finder ShowHiddenFiles -bool true |
osascript -e | Run AppleScript | $ osascript -e 'display notification "Done"' |
Spotlight
| Command | Description | Example |
|---|---|---|
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
| Command | Description | Example |
|---|---|---|
log show | View unified log | $ log show --last 1h |
log show --predicate | Filter logs | $ log show --predicate 'process == "Safari"' |
log stream | Real-time log stream | $ log stream --level debug |
console | Open Console.app | $ open -a Console |
syslog | Legacy system log (deprecated) | $ syslog |
dmesg | Kernel messages | $ dmesg |
system_profiler SPLogsDataType | System logs info | $ system_profiler SPLogsDataType |
Security Commands
| Command | Description | Example |
|---|---|---|
csrutil status | SIP status | $ csrutil status |
spctl --status | Gatekeeper 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-keychains | List keychains | $ security list-keychains |
security find-identity -v | List signing identities | $ security find-identity -v |
security find-generic-password | Find password | $ security find-generic-password -s "service" |
fdesetup status | FileVault status | $ fdesetup status |
sudo spctl --master-disable | Disable Gatekeeper | # spctl --master-disable |
Remote Access
| Command | Description | Example |
|---|---|---|
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
| Command | Description | Example |
|---|---|---|
history | Show 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+R | Reverse search history | |
alias | Show 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" |
env | Show environment | $ env |
printenv | Print environment variables | $ printenv |
echo $<var> | Print variable | $ echo $HOME |
Getting Help
| Command | Description | Example |
|---|---|---|
man <command> | Manual page | $ man ls |
man -k <term> | Search manual pages | $ man -k network |
apropos <term> | Same as man -k | $ apropos copy |
<command> --help | Command help | $ git --help |
<command> -h | Short help | $ grep -h |
whatis <command> | One-line description | $ whatis curl |
info <command> | GNU info pages | $ info bash |
Quick Reference: Keyboard Shortcuts in Commands
| Shortcut | Action |
|---|---|
Ctrl+C | Cancel/interrupt |
Ctrl+D | EOF/logout |
Ctrl+Z | Suspend process |
Ctrl+L | Clear screen |
Ctrl+A | Beginning of line |
Ctrl+E | End of line |
Ctrl+U | Clear line before cursor |
Ctrl+K | Clear line after cursor |
Ctrl+W | Delete word before cursor |
Ctrl+R | Reverse search history |
Tab | Auto-complete |
Tab Tab | Show 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)
| File | Scope | Purpose |
|---|---|---|
/etc/zshenv | System | Read for all zsh invocations |
/etc/zprofile | System | Read for login shells |
/etc/zshrc | System | Read for interactive shells |
/etc/zlogin | System | Read after zprofile for login shells |
~/.zshenv | User | User environment (all invocations) |
~/.zprofile | User | User login shell setup |
~/.zshrc | User | User interactive shell config |
~/.zlogin | User | User post-login commands |
~/.zlogout | User | Logout cleanup |
Load order for interactive login shell:
/etc/zshenv~/.zshenv/etc/zprofile~/.zprofile/etc/zshrc~/.zshrc/etc/zlogin~/.zlogin
Bash
| File | Scope | Purpose |
|---|---|---|
/etc/profile | System | Login shell initialization |
/etc/bashrc | System | Non-login interactive shells |
~/.bash_profile | User | Login shell (read instead of .profile) |
~/.bash_login | User | Login shell (fallback) |
~/.profile | User | Login shell (fallback if no .bash_profile) |
~/.bashrc | User | Interactive non-login shells |
~/.bash_logout | User | Logout cleanup |
Note: On macOS, Terminal.app opens login shells by default, so
~/.bash_profileis used rather than~/.bashrc. Many users source.bashrcfrom.bash_profilefor consistency.
Shell-Agnostic
| File | Purpose |
|---|---|
~/.inputrc | GNU Readline configuration |
/etc/paths | System PATH directories (one per line) |
/etc/paths.d/* | Additional PATH directories |
/etc/manpaths | Manual page paths |
/etc/manpaths.d/* | Additional manual paths |
Application Preferences (Property Lists)
macOS applications store preferences in property list (plist) files:
User Preferences
| Location | Contents |
|---|---|
~/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
| Location | Contents |
|---|---|
/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
| Location | Runs As | Loaded When |
|---|---|---|
~/Library/LaunchAgents/ | Current user | User logs in |
/Library/LaunchAgents/ | Current user | User logs in |
/System/Library/LaunchAgents/ | Current user | User logs in (Apple only) |
/Library/LaunchDaemons/ | root (or specified user) | System boot |
/System/Library/LaunchDaemons/ | root | System 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
| Location | Contents |
|---|---|
~/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
| Location | Contents |
|---|---|
/Library/Application Support/ | System-wide app data |
/Library/Caches/ | System caches |
/Library/Logs/ | System and app logs |
Development Tools
Xcode and Command Line Tools
| Location | Contents |
|---|---|
/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
| Tool | Configuration | Versions Location |
|---|---|---|
| pyenv | ~/.pyenv/ | ~/.pyenv/versions/ |
| rbenv | ~/.rbenv/ | ~/.rbenv/versions/ |
| nvm | ~/.nvm/ | ~/.nvm/versions/ |
| nodenv | ~/.nodenv/ | ~/.nodenv/versions/ |
| rustup | ~/.rustup/ | ~/.rustup/toolchains/ |
Network Configuration
DNS
| Location | Purpose |
|---|---|
/etc/resolv.conf | DNS resolver configuration (often managed by macOS) |
/etc/hosts | Static hostname mappings |
/Library/Preferences/SystemConfiguration/preferences.plist | Network service configuration |
# View DNS configuration
$ scutil --dns
# View network configuration
$ cat /Library/Preferences/SystemConfiguration/preferences.plist | plutil -convert xml1 -o - -
SSH
| Location | Purpose |
|---|---|
~/.ssh/config | User SSH client configuration |
~/.ssh/known_hosts | Known host keys |
~/.ssh/authorized_keys | Keys allowed to log in as you |
~/.ssh/id_rsa, ~/.ssh/id_ed25519 | Private keys |
~/.ssh/id_rsa.pub, ~/.ssh/id_ed25519.pub | Public keys |
/etc/ssh/ssh_config | System-wide SSH client config |
/etc/ssh/sshd_config | SSH server configuration |
Firewall
| Location | Purpose |
|---|---|
/etc/pf.conf | Packet Filter configuration |
/etc/pf.anchors/ | PF anchor rules |
/Library/Preferences/com.apple.alf.plist | Application Layer Firewall settings |
Security
Keychain
| Location | Purpose |
|---|---|
~/Library/Keychains/login.keychain-db | User login keychain |
/Library/Keychains/System.keychain | System keychain |
/System/Library/Keychains/SystemRootCertificates.keychain | Root certificates |
TCC (Privacy) Database
| Location | Purpose |
|---|---|
~/Library/Application Support/com.apple.TCC/TCC.db | User privacy permissions |
/Library/Application Support/com.apple.TCC/TCC.db | System 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:
| Location | Purpose |
|---|---|
/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
| Location | Contents |
|---|---|
/var/log/system.log | General system log (limited) |
/var/log/install.log | Installation logs |
/var/log/wifi.log | Wi-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
| Purpose | macOS | Linux |
|---|---|---|
| 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 config | Embedded 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
| Purpose | macOS | Linux |
|---|---|---|
| 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
| Shortcut | Action |
|---|---|
Cmd+N | New window |
Cmd+T | New tab |
Cmd+Shift+N | New window with same command |
Cmd+W | Close tab/window |
Cmd+Shift+W | Close window |
Cmd+1-9 | Switch to tab 1-9 |
Cmd+Shift+[ | Previous tab |
Cmd+Shift+] | Next tab |
Cmd+Left Arrow | Previous tab |
Cmd+Right Arrow | Next tab |
Cmd+Shift+D | Split pane horizontally |
Cmd+D | Split pane vertically (some configs) |
Cmd+Shift+Enter | Toggle full screen |
Cmd+Ctrl+F | Toggle full screen |
Text and Display
| Shortcut | Action |
|---|---|
Cmd++ or Cmd+= | Increase font size |
Cmd+- | Decrease font size |
Cmd+0 | Reset font size to default |
Cmd+K | Clear screen and scrollback |
Cmd+L | Clear screen (keep scrollback) |
Ctrl+L | Clear screen (shell command) |
Cmd+Home | Scroll to top |
Cmd+End | Scroll to bottom |
Page Up | Scroll up one page |
Page Down | Scroll down one page |
Cmd+Up Arrow | Scroll up one line |
Cmd+Down Arrow | Scroll down one line |
Selection and Clipboard
| Shortcut | Action |
|---|---|
Cmd+A | Select all |
Cmd+C | Copy selection |
Cmd+V | Paste |
Cmd+Shift+V | Paste escaped (for URLs, paths) |
Double-click | Select word |
Triple-click | Select line |
Cmd+Click | Open URL in browser |
Option+Click | Position cursor at click location |
Cmd+Drag | Rectangular selection |
Search
| Shortcut | Action |
|---|---|
Cmd+F | Find |
Cmd+G | Find next |
Cmd+Shift+G | Find previous |
Cmd+E | Use selection for find |
Cmd+J | Jump to selection |
Marks and Bookmarks
| Shortcut | Action |
|---|---|
Cmd+U | Mark current line |
Cmd+Shift+U | Mark line and send return |
Cmd+Shift+M | Insert bookmark |
Cmd+Up Arrow | Jump to previous mark |
Cmd+Down Arrow | Jump to next mark |
Cmd+Shift+A | Select 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
| Shortcut | Action |
|---|---|
Ctrl+A | Move to beginning of line |
Ctrl+E | Move to end of line |
Ctrl+F | Move forward one character |
Ctrl+B | Move backward one character |
Option+F or Esc F | Move forward one word |
Option+B or Esc B | Move backward one word |
Ctrl+XX | Toggle between start of line and current position |
Note: On macOS,
Option+FandOption+Bmay require enabling “Use Option as Meta key” in Terminal preferences, or useEscfollowed by the letter.
Deletion
| Shortcut | Action |
|---|---|
Ctrl+D | Delete character under cursor (or logout if empty line) |
Ctrl+H | Delete character before cursor (backspace) |
Ctrl+W | Delete word before cursor |
Option+D or Esc D | Delete word after cursor |
Ctrl+U | Delete from cursor to beginning of line |
Ctrl+K | Delete from cursor to end of line |
Ctrl+Y | Paste (yank) last deleted text |
Option+Y | Cycle through kill ring |
Text Manipulation
| Shortcut | Action |
|---|---|
Ctrl+T | Transpose characters (swap current and previous) |
Option+T or Esc T | Transpose words |
Option+U or Esc U | Uppercase word from cursor |
Option+L or Esc L | Lowercase word from cursor |
Option+C or Esc C | Capitalize word from cursor |
History Navigation
| Shortcut | Action |
|---|---|
Ctrl+P or Up Arrow | Previous command in history |
Ctrl+N or Down Arrow | Next command in history |
Ctrl+R | Reverse incremental search |
Ctrl+S | Forward incremental search (may need to enable) |
Ctrl+G | Cancel search and restore original line |
Option+< or Esc < | First command in history |
Option+> or Esc > | Last command in history |
Ctrl+O | Execute command and fetch next from history |
!! | Repeat last command (type and press Enter) |
!n | Repeat command number n from history |
!string | Repeat last command starting with string |
!?string | Repeat last command containing string |
Process Control
| Shortcut | Action |
|---|---|
Ctrl+C | Interrupt (SIGINT) - cancel current command |
Ctrl+Z | Suspend (SIGTSTP) - pause current command |
Ctrl+D | End of file (EOF) - logout or close input |
Ctrl+\ | Quit (SIGQUIT) - forceful termination |
Ctrl+S | Pause output (XOFF) |
Ctrl+Q | Resume output (XON) |
Completion
| Shortcut | Action |
|---|---|
Tab | Auto-complete command, filename, or variable |
Tab Tab | Show 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
| Shortcut | Action |
|---|---|
Ctrl+L | Clear screen, redraw current line at top |
Ctrl+S | Stop output to screen |
Ctrl+Q | Resume output to screen |
Zsh-Specific Shortcuts
Zsh includes additional features beyond standard Emacs bindings:
Zsh Expansion
| Shortcut | Action |
|---|---|
Tab | Complete and show menu if ambiguous |
Ctrl+I | Same as Tab |
Shift+Tab | Reverse through completions |
Option+H or Esc H | Run help for current command |
Option+? | Show command help |
Ctrl+X A | Expand alias |
Ctrl+X G | List expansions of current glob |
Ctrl+X * | Expand glob inline |
Zsh History
| Shortcut | Action |
|---|---|
Ctrl+R | Incremental history search |
Ctrl+P / Ctrl+N | Navigate history |
Option+P | History search backward (prefix match) |
Option+N | History search forward (prefix match) |
fc | Edit last command in editor |
r | Re-run last command |
r foo=bar | Re-run last command, replacing foo with bar |
Zsh Line Editor (ZLE)
| Shortcut | Action |
|---|---|
Ctrl+X Ctrl+E | Edit command line in $EDITOR |
Option+Q | Push line to buffer, clear, execute next command, then restore |
Option+' | Quote line |
Option+" | Quote region |
Ctrl+X Ctrl+V | Show 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)
| Key | Action |
|---|---|
h | Move left |
l | Move right |
w | Move forward one word |
b | Move backward one word |
e | Move to end of word |
0 | Move to beginning of line |
$ | Move to end of line |
^ | Move to first non-blank character |
x | Delete character |
dw | Delete word |
dd | Delete line |
d$ or D | Delete to end of line |
d0 | Delete to beginning of line |
cw | Change word |
cc | Change line |
c$ or C | Change to end of line |
yy | Yank (copy) line |
yw | Yank word |
p | Paste after cursor |
P | Paste before cursor |
u | Undo |
Ctrl+R | Redo |
i | Insert mode at cursor |
I | Insert at beginning of line |
a | Append after cursor |
A | Append at end of line |
r | Replace single character |
R | Replace mode |
k | Previous history |
j | Next history |
/ | Search forward in history |
? | Search backward in history |
n | Repeat search |
N | Repeat search in reverse |
v | Edit command in $EDITOR |
Vi Insert Mode
| Key | Action |
|---|---|
Escape | Return to command mode |
Ctrl+[ | Return to command mode |
Ctrl+C | Cancel and return to command mode |
iTerm2 Shortcuts
iTerm2 provides additional shortcuts beyond Terminal.app:
Windows and Tabs
| Shortcut | Action |
|---|---|
Cmd+N | New window |
Cmd+T | New tab |
Cmd+W | Close tab |
Cmd+Shift+W | Close window |
Cmd+Option+W | Close all tabs except current |
Cmd+1-9 | Switch to tab |
Cmd+Left/Right | Previous/next tab |
Cmd+Shift+Enter | Maximize pane |
Cmd+Option+E | Expose all tabs |
Panes (Split View)
| Shortcut | Action |
|---|---|
Cmd+D | Split vertically |
Cmd+Shift+D | Split horizontally |
Cmd+Option+Arrow | Navigate between panes |
Cmd+] | Next pane |
Cmd+[ | Previous pane |
Cmd+Shift+Enter | Toggle pane zoom |
Cmd+Option+Shift+H/V | Move divider |
Search and Selection
| Shortcut | Action |
|---|---|
Cmd+F | Find |
Cmd+Shift+H | Paste history |
Cmd+; | Autocomplete |
Cmd+Shift+; | Open command history |
Cmd+Option+/ | Recent directories popup |
Cmd+Click | Open URL/file |
Cmd+Option+B | Instant replay |
Text
| Shortcut | Action |
|---|---|
Cmd+K | Clear buffer |
Cmd+Ctrl+K | Clear scrollback |
Cmd+/ | Find cursor |
Cmd+Option+; | Open command history |
Cmd+Shift+M | Set mark |
Cmd+Shift+J | Jump to mark |
Profiles and Settings
| Shortcut | Action |
|---|---|
Cmd+I | Edit session |
Cmd+, | Preferences |
Cmd+Option+I | Toggle broadcast input |
Cmd+Shift+O | Open quickly (fuzzy search tabs) |
Less Pager Shortcuts
When viewing files with less:
| Key | Action |
|---|---|
Space or f | Forward one page |
b | Backward one page |
d | Forward half page |
u | Backward half page |
j or Down | Forward one line |
k or Up | Backward one line |
g or Home | Go to beginning |
G or End | Go to end |
/<pattern> | Search forward |
?<pattern> | Search backward |
n | Next search match |
N | Previous search match |
&<pattern> | Show only matching lines |
m<letter> | Mark current position |
'<letter> | Go to mark |
F | Follow mode (like tail -f) |
v | Open in $EDITOR |
-N | Toggle line numbers |
-S | Toggle line wrapping |
h | Help |
q | Quit |
Man Page Shortcuts
Man pages use less by default, but some additional keys work:
| Key | Action |
|---|---|
h | Help |
q | Quit |
Space | Next page |
b | Previous page |
/ | Search |
n | Next match |
N | Previous match |
Vim Quick Reference
For quick edits in vim:
Normal Mode
| Key | Action |
|---|---|
i | Insert before cursor |
a | Insert after cursor |
o | Insert new line below |
O | Insert new line above |
x | Delete character |
dd | Delete line |
yy | Copy line |
p | Paste |
u | Undo |
Ctrl+R | Redo |
/ | Search |
n | Next match |
:w | Save |
:q | Quit |
:wq | Save and quit |
:q! | Quit without saving |
ZZ | Save and quit |
ZQ | Quit without saving |
Quick Reference Card
Essential Shortcuts (Memorize These)
| Shortcut | Action |
|---|---|
Ctrl+C | Cancel/interrupt |
Ctrl+D | Exit/EOF |
Ctrl+Z | Suspend |
Ctrl+L | Clear screen |
Ctrl+A | Start of line |
Ctrl+E | End of line |
Ctrl+U | Delete to start |
Ctrl+K | Delete to end |
Ctrl+W | Delete word |
Ctrl+R | Search history |
Tab | Autocomplete |
Up/Down | History navigation |
Terminal.app Essentials
| Shortcut | Action |
|---|---|
Cmd+T | New tab |
Cmd+W | Close tab |
Cmd+1-9 | Switch tab |
Cmd+K | Clear all |
Cmd+F | Find |
Cmd++/- | Font size |
Process Control
| Shortcut | Action |
|---|---|
Ctrl+C | Kill foreground |
Ctrl+Z | Suspend |
bg | Continue in background |
fg | Bring to foreground |
jobs | List 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 -leand modify withchmod +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
.appextension 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-sdprovides 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
clangor throughgcc(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 withexport 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 withls -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). Runbrew 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
securitycommand.
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
otoolorfile. - 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
nvramcommand.
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
psor 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
defaultsorplutil. - 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 forcom.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
sudoto 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.
- Symlink (Symbolic Link)
- 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
- TCC (Transparency, Consent, and Control)
- 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
logcommand or Console.app. - Universal Binary
- An executable containing code for multiple CPU architectures (e.g., arm64 and x86_64). View architectures with
lipo -infoorfile. - 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
KeepAlivein 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 fileto 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
| Version | Name | Release Year | Notable Changes |
|---|---|---|---|
| 10.0 | Cheetah | 2001 | First Mac OS X release |
| 10.4 | Tiger | 2005 | Spotlight, Dashboard |
| 10.5 | Leopard | 2007 | Time Machine, Spaces |
| 10.6 | Snow Leopard | 2009 | Grand Central Dispatch |
| 10.7 | Lion | 2011 | Full-screen apps, Launchpad |
| 10.9 | Mavericks | 2013 | Free upgrades begin |
| 10.11 | El Capitan | 2015 | System Integrity Protection |
| 10.12 | Sierra | 2016 | Siri, APFS introduced |
| 10.13 | High Sierra | 2017 | APFS default for SSD |
| 10.14 | Mojave | 2018 | Dark mode, privacy controls |
| 10.15 | Catalina | 2019 | zsh default, read-only system |
| 11 | Big Sur | 2020 | Apple Silicon support |
| 12 | Monterey | 2021 | Shortcuts, Focus modes |
| 13 | Ventura | 2022 | Stage Manager |
| 14 | Sonoma | 2023 | Desktop widgets |
| 15 | Sequoia | 2024 | iPhone mirroring |
Filesystem Path Quick Reference
| Path | Contents |
|---|---|
/ | 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
| Resource | URL | Description |
|---|---|---|
| Apple Developer Documentation | developer.apple.com/documentation | Official API and framework documentation |
| Mac Technology Overview | developer.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_Overview | System architecture overview |
| Shell Scripting Primer | developer.apple.com/library/archive/documentation/OpenSource/Conceptual/ShellScripting | Apple’s shell scripting guide |
| Daemons and Services | developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup | launchd and services guide |
| File System Programming Guide | developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide | Filesystem concepts and APIs |
| Security Overview | developer.apple.com/documentation/security | Security frameworks and features |
System Administration
| Resource | URL | Description |
|---|---|---|
| Apple Platform Security | support.apple.com/guide/security | Security architecture guide |
| macOS Deployment Reference | support.apple.com/guide/deployment | Enterprise deployment |
| Mac Admins Documentation | support.apple.com/guide/mac-help | End-user documentation |
| Apple Configurator Guide | support.apple.com/guide/apple-configurator-mac | Device configuration |
Open Source
| Resource | URL | Description |
|---|---|---|
| Apple Open Source | opensource.apple.com | Darwin and related source code |
| XNU Source | github.com/apple-oss-distributions/xnu | XNU kernel source |
| Swift Source | github.com/apple/swift | Swift 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
| Resource | URL | Description |
|---|---|---|
| macOS Man Pages | keith.github.io/xcode-man-pages | Searchable macOS man pages |
| FreeBSD Man Pages | freebsd.org/cgi/man.cgi | BSD reference (often applicable to macOS) |
| man7.org | man7.org/linux/man-pages | Linux man pages (for comparison) |
| explainshell.com | explainshell.com | Visual command explanation |
Package Managers
Homebrew
| Resource | URL | Description |
|---|---|---|
| Homebrew | brew.sh | Main site and installation |
| Homebrew Documentation | docs.brew.sh | Official documentation |
| Homebrew Formulae | formulae.brew.sh | Package search |
| Homebrew GitHub | github.com/Homebrew/brew | Source code and issues |
MacPorts
| Resource | URL | Description |
|---|---|---|
| MacPorts | macports.org | Main site |
| MacPorts Guide | guide.macports.org | Documentation |
| Port Search | ports.macports.org | Package search |
Other Package Systems
| Resource | URL | Description |
|---|---|---|
| Nix on macOS | nixos.org/download.html | Nix package manager |
| pkgsrc | pkgsrc.org | NetBSD’s portable package system |
Shell Resources
Zsh
| Resource | URL | Description |
|---|---|---|
| Zsh Manual | zsh.sourceforge.io/Doc | Official documentation |
| Oh My Zsh | ohmyz.sh | Zsh framework and plugins |
| Prezto | github.com/sorin-ionescu/prezto | Alternative zsh framework |
| Zsh Users | github.com/zsh-users | Popular zsh plugins |
| Awesome Zsh | github.com/unixorn/awesome-zsh-plugins | Curated plugin list |
Bash
| Resource | URL | Description |
|---|---|---|
| Bash Manual | gnu.org/software/bash/manual | Official documentation |
| Bash Guide | mywiki.wooledge.org/BashGuide | Community guide |
| Bash Pitfalls | mywiki.wooledge.org/BashPitfalls | Common mistakes |
| ShellCheck | shellcheck.net | Shell script linter |
General Shell
| Resource | URL | Description |
|---|---|---|
| The Art of Command Line | github.com/jlevy/the-art-of-command-line | Command line mastery |
| Command Line Power User | commandlinepoweruser.com | Video course |
| tldr pages | tldr.sh | Simplified man pages |
Terminal Emulators
| Application | URL | Description |
|---|---|---|
| Terminal.app | Built-in | macOS default terminal |
| iTerm2 | iterm2.com | Feature-rich terminal |
| Alacritty | alacritty.org | GPU-accelerated terminal |
| kitty | sw.kovidgoyal.net/kitty | Fast, feature-rich terminal |
| Warp | warp.dev | Modern terminal with AI |
| Hyper | hyper.is | Electron-based terminal |
| Tabby | tabby.sh | Cross-platform terminal |
Text Editors
Terminal-Based
| Editor | URL | Description |
|---|---|---|
| Vim | vim.org | Classic modal editor |
| Neovim | neovim.io | Modern Vim fork |
| GNU Emacs | gnu.org/software/emacs | Extensible editor |
| nano | Built-in | Simple editor |
| micro | micro-editor.github.io | Modern terminal editor |
GUI with Terminal Integration
| Editor | URL | Description |
|---|---|---|
| Visual Studio Code | code.visualstudio.com | Popular extensible editor |
| Sublime Text | sublimetext.com | Fast, powerful editor |
| BBEdit | barebones.com/products/bbedit | macOS-native text editor |
Development Resources
Command Line Tools
| Resource | URL | Description |
|---|---|---|
| Xcode Downloads | developer.apple.com/download | Xcode and tools |
| Xcode Release Notes | developer.apple.com/documentation/xcode-release-notes | Version history |
Version Control
| Resource | URL | Description |
|---|---|---|
| Pro Git Book | git-scm.com/book | Comprehensive Git book |
| GitHub CLI | cli.github.com | GitHub command line |
| Git Documentation | git-scm.com/doc | Official documentation |
Language-Specific
| Resource | URL | Description |
|---|---|---|
| pyenv | github.com/pyenv/pyenv | Python version management |
| rbenv | github.com/rbenv/rbenv | Ruby version management |
| nvm | github.com/nvm-sh/nvm | Node.js version management |
| rustup | rustup.rs | Rust toolchain installer |
System Administration
Mac Admin Resources
| Resource | URL | Description |
|---|---|---|
| Mac Admins Foundation | macadmins.org | Community resources |
| MacAdmins Slack | macadmins.slack.com | Community chat |
| Der Flounder | derflounder.wordpress.com | Mac admin blog |
| Mr. Macintosh | mrmacintosh.com | macOS news and guides |
| Scripting OS X | scriptingosx.com | Automation and scripting |
| Mac Admin Info | macadmin.info | Tools and resources |
Configuration Management
| Tool | URL | Description |
|---|---|---|
| Munki | github.com/munki/munki | Software deployment |
| Jamf | jamf.com | Enterprise management |
| Mosyle | mosyle.com | Apple device management |
| Puppet | puppet.com | Configuration management |
| Ansible | ansible.com | Automation platform |
| Chef | chef.io | Infrastructure automation |
Security Tools
| Tool | URL | Description |
|---|---|---|
| Objective-See Tools | objective-see.org/tools.html | Free security tools |
| Santa | github.com/google/santa | Application allowlisting |
| osquery | osquery.io | System information via SQL |
| Lulu | objective-see.org/products/lulu.html | Open-source firewall |
Books
macOS and Unix
| Title | Author | Description |
|---|---|---|
| macOS Internals | Jonathan Levin | Deep dive into macOS architecture |
| The Mac Hacker’s Handbook | Miller & Dai Zovi | macOS security |
| Learning Unix for OS X | Dave Taylor | Introduction for Mac users |
| Mac OS X for Unix Geeks | Jepson & Rothman | Unix perspective on macOS |
Unix and Linux
| Title | Author | Description |
|---|---|---|
| The Linux Command Line | William Shotts | Free online |
| Unix and Linux System Administration Handbook | Nemeth et al. | Comprehensive sysadmin |
| How Linux Works | Brian Ward | Understanding Linux internals |
| The Unix Programming Environment | Kernighan & Pike | Classic Unix philosophy |
| Advanced Programming in the Unix Environment | Stevens & Rago | Unix programming bible |
Shell Scripting
| Title | Author | Description |
|---|---|---|
| Learning the bash Shell | Newham & Rosenblatt | Bash fundamentals |
| Classic Shell Scripting | Robbins & Beebe | POSIX shell scripting |
| Wicked Cool Shell Scripts | Taylor & Perry | Practical scripts |
| From Bash to Z Shell | Kiddle et al. | Advanced shell usage |
Community Resources
Forums and Q&A
| Resource | URL | Description |
|---|---|---|
| Stack Overflow | stackoverflow.com/questions/tagged/macos | Programming Q&A |
| Ask Different | apple.stackexchange.com | Apple-focused Q&A |
| Unix & Linux Stack Exchange | unix.stackexchange.com | Unix Q&A |
| Super User | superuser.com | Power user Q&A |
Discussion
| Resource | URL | Description |
|---|---|---|
| MacRumors Forums | forums.macrumors.com | Mac community |
| r/MacOS | reddit.com/r/MacOS | macOS subreddit |
| r/commandline | reddit.com/r/commandline | CLI subreddit |
| r/osx | reddit.com/r/osx | Legacy macOS subreddit |
| Hacker News | news.ycombinator.com | Tech news and discussion |
Conferences and Events
| Event | URL | Description |
|---|---|---|
| WWDC | developer.apple.com/wwdc | Apple developer conference |
| MacDevOps:YVR | macdevops.ca | Mac admin conference |
| MacSysAdmin | macsysadmin.se | European Mac admin conference |
| Objective by the Sea | objectivebythesea.org | macOS security conference |
Blogs and News
Technical Blogs
| Resource | URL | Description |
|---|---|---|
| Eclectic Light | eclecticlight.co | macOS technical deep-dives |
| The Eclectic Light Company | eclecticlight.co/tag/macs/ | Howard Oakley’s blog |
| Scripting OS X | scriptingosx.com | Armin Briegel’s blog |
| Der Flounder | derflounder.wordpress.com | Rich Trouton’s blog |
| Sixcolors | sixcolors.com | Jason Snell’s Apple coverage |
News Sites
| Resource | URL | Description |
|---|---|---|
| MacRumors | macrumors.com | Apple news |
| 9to5Mac | 9to5mac.com | Apple news |
| Ars Technica | arstechnica.com/apple | In-depth Apple coverage |
| AppleInsider | appleinsider.com | Apple news and reviews |
Useful Utilities
System Utilities
| Tool | URL | Description |
|---|---|---|
| htop | brew install htop | Interactive process viewer |
| ncdu | brew install ncdu | NCurses disk usage |
| tree | brew install tree | Directory tree listing |
| jq | brew install jq | JSON processor |
| ripgrep | brew install ripgrep | Fast search tool |
| fd | brew install fd | Fast find alternative |
| bat | brew install bat | Cat with syntax highlighting |
| exa/eza | brew install eza | Modern ls replacement |
| fzf | brew install fzf | Fuzzy finder |
| tmux | brew install tmux | Terminal multiplexer |
macOS-Specific
| Tool | URL | Description |
|---|---|---|
| mas | brew install mas | Mac App Store CLI |
| duti | brew install duti | Set default applications |
| m-cli | brew install m-cli | macOS CLI swiss army knife |
| mackup | brew install mackup | Application settings backup |
| trash | brew install trash | Move files to Trash |
| terminal-notifier | brew install terminal-notifier | Send notifications |
| blueutil | brew install blueutil | Bluetooth CLI |
| switchaudio-osx | brew install switchaudio-osx | Audio device switching |
Learning Paths
Beginner
- Read Apple’s Shell Scripting Primer
- Work through The Linux Command Line (free online)
- Practice with tldr pages and explainshell
- Install Homebrew and explore packages
- Learn basic vim or nano for quick edits
Intermediate
- Master zsh configuration and plugins
- Learn shell scripting and automation
- Understand launchd for services
- Explore system administration tools
- Study filesystem hierarchy and permissions
Advanced
- Read macOS Internals by Jonathan Levin
- Explore XNU source code
- Study security architecture (Gatekeeper, SIP, TCC)
- Learn IOKit and system frameworks
- Contribute to open-source macOS tools
Quick Reference Links
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/