BSD vs GNU: The Commands That Will Catch You Off Guard
The single most common mistake Linux admins make on macOS is assuming GNU coreutils behavior. macOS ships with BSD versions of ls, find, xargs, sed, awk, and date. The differences are not cosmetic.
The `sed` on macOS requires an extension argument for in-place editing, even if you pass an empty string. On Linux you write `sed -i 's/foo/bar/' file.txt`. On macOS that fails with 'invalid command code'. The correct macOS syntax is `sed -i '' 's/foo/bar/' file.txt`. This breaks scripts constantly when people run Linux-written automation on Mac endpoints.
The `find` command on macOS does not support `-printf`. If your scripts use it for formatting output, you need to either install GNU findutils via Homebrew or rewrite using `-exec printf` patterns. Similarly, `xargs` on macOS does not support `-d` to specify a delimiter, which matters when filenames contain spaces.
Install GNU coreutils if you need consistent behavior across platforms. On a managed fleet we recommend making this explicit rather than relying on PATH shadowing.
# Install GNU coreutils via Homebrew
brew install coreutils findutils gnu-sed gawk
# GNU tools are prefixed with 'g' by default
# gfind, gsed, gawk, gdate, gls
# To use without prefix, add to PATH in .zshrc
export PATH="$(brew --prefix)/opt/coreutils/libexec/gnubin:$PATH"
export PATH="$(brew --prefix)/opt/findutils/libexec/gnubin:$PATH"
export PATH="$(brew --prefix)/opt/gnu-sed/libexec/gnubin:$PATH"
# Verify which find you're calling
which find
find --version
APFS Volumes, Firmlinks, and What ls Is Not Showing You
APFS introduced firmlinks in macOS Catalina (10.15), and they look like directories but behave like bidirectional hard links between volumes. The classic example is `/var`, which appears to live on the root volume but is actually a firmlink to `/private/var`. Running `ls -la /` shows `var -> private/var` as a symlink, but it is not a symlink - it is a firmlink, and the distinction matters when you are calculating disk usage or writing backup scripts.
`du -sh /var` and `du -sh /private/var` will give you different numbers depending on which APFS volume boundary you are crossing. For accurate disk accounting on macOS, use `diskutil` rather than `df` when you need volume-level information, and use `du` with caution near firmlink boundaries.
APFS also supports clones at the file level, which is what makes `cp` fast on macOS when copying within the same volume. Since macOS 10.12 Sierra, `cp` uses `clonefile()` by default when source and destination are on the same APFS volume. The copy appears instantaneous and uses no additional disk space until one of the copies is modified (copy-on-write). This is excellent for staging build artifacts but means your `du` output after a `cp` operation will not show the expected increase.
To check whether a file is a clone, use `stat` and look at the block count relative to file size. A cloned file that has not diverged will show far fewer blocks than its byte size would suggest.
# List APFS volumes on the system
diskutil list
diskutil apfs list
# Check actual vs apparent size - cloned file example
cp largefile.bin largefile_clone.bin
stat largefile.bin largefile_clone.bin
# Note: blocks field will be identical and low for clones
# Get real disk usage excluding firmlink traversal
du -sxh /private/var
# -x flag: stay on one filesystem, does not cross mount points
# Check if a path is a firmlink
diskutil info / | grep 'APFS'
ls -la / | grep ' -> '
Extended Attributes and Resource Forks
macOS attaches extended attributes to files for quarantine flags, Finder metadata, Spotlight indexing hints, and ACLs. These attributes travel with files when you copy within APFS, but they are stripped or mangled by rsync without the right flags, by tar without `-x`, and entirely by `scp`. On production systems this causes two classes of problems: quarantine flags that block execution of deployed binaries, and lost metadata that breaks Finder labels or Time Machine tracking.
The quarantine extended attribute `com.apple.quarantine` is the most operationally significant. Any file downloaded via a browser or some download tools gets this attribute. When Gatekeeper sees it, it blocks execution and may trigger notarization checks. On a deployment pipeline, if your artifact drops onto a Mac endpoint with this attribute set, automated startup will fail silently or with a misleading dialog.
Use `xattr` to inspect and remove extended attributes. For bulk removal in deployment scripts, the recursive flag `-r` combined with `-d` for delete is the pattern we use.
# List all extended attributes on a file
xattr -l /path/to/file
# Show specific attribute value
xattr -p com.apple.quarantine /path/to/downloaded.app
# Remove quarantine attribute from a single file
xattr -d com.apple.quarantine /path/to/file
# Remove quarantine recursively from a directory (deployment use case)
xattr -rd com.apple.quarantine /path/to/app.app
# List all xattrs across a directory tree
find /path/to/dir -exec xattr -l {} + 2>/dev/null
# Copy file preserving extended attributes (use cp -p or cp -a)
cp -a source.file dest.file
# rsync with extended attribute and ACL preservation
rsync -aX --acls source/ dest/
# -X preserves xattrs, --acls (or -A) preserves ACLs
find on macOS: Practical Patterns for Fleet Operations
BSD `find` on macOS supports most of the predicates you use on Linux but with different flag syntax in some cases. The `-mtime` behavior is identical: `-mtime -1` means modified within the last 24 hours. The `-newer` flag works the same way. Where it diverges is in output formatting and some flag ordering requirements.
macOS `find` is strict about flag ordering. The expression must come after the path. `find -name '*.log' /var/log` fails on macOS where it might work on some Linux implementations. Always write `find /var/log -name '*.log'`.
For time-based file management - rotating logs, cleaning build artifacts, archiving old project directories - the combination of `find` with `-mtime`, `-delete`, and `-exec` covers most production scenarios. We ran the following pattern on a build server managing Xcode derived data, which grows to 40-80GB within two weeks of active use.
# Find and delete Xcode DerivedData older than 14 days
find ~/Library/Developer/Xcode/DerivedData \
-mindepth 1 -maxdepth 1 \
-type d \
-mtime +14 \
-exec rm -rf {} +
# Find files larger than 100MB modified in the last 7 days
find ~ -type f -size +100M -mtime -7 2>/dev/null
# Find all .DS_Store files and delete them (pre-deploy cleanup)
find /path/to/project -name '.DS_Store' -delete
# Find files with specific extended attribute
find /Applications -name '*.app' -exec \
sh -c 'xattr "$1" | grep -q quarantine && echo "$1"' _ {} \;
# Count files by extension in a directory tree
find . -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -20
# Find broken symlinks
find /usr/local -type l ! -exec test -e {} \; -print
rsync on macOS: What Changes and What Breaks
macOS ships with rsync 2.6.9, which is from 2006. This is not a typo. Apple stopped updating the bundled rsync years ago, and the version difference between 2.6.9 and the current 3.x branch means missing flags you likely depend on: `--info=progress2`, `--human-readable` in the way you expect it, and proper handling of some macOS-specific metadata.
Install rsync 3.x via Homebrew for any serious use. The Homebrew version as of mid-2026 is 3.3.0 and includes proper HFS+ and APFS metadata support when compiled with `--enable-hfs-compression`.
For macOS-to-macOS transfers where you need to preserve Finder metadata, Time Machine history hints, and resource forks, use `rsync -aHAX`. The `-H` flag preserves hard links, `-A` preserves ACLs, and `-X` preserves extended attributes. Without these, you can transfer files that look correct but lose executable permissions set via ACLs, or quarantine attributes that were deliberately absent.
For transfers to Linux destinations, strip macOS-specific attributes explicitly so you do not litter the destination with `._` AppleDouble files. The `--exclude='._*'` and `--exclude='.DS_Store'` flags are the minimum. For team projects, putting this in an `.rsync-filter` file committed to the repository prevents repeated mistakes.
# Install current rsync
brew install rsync
# Verify version
$(brew --prefix)/bin/rsync --version | head -1
# Full macOS-to-macOS sync preserving all metadata
rsync -aHAX --progress \
/source/path/ \
/destination/path/
# macOS-to-Linux: strip Apple-specific files
rsync -avz \
--exclude='._*' \
--exclude='.DS_Store' \
--exclude='__MACOSX' \
/local/path/ \
user@linux-host:/remote/path/
# Use filter file for project-level config
# .rsync-filter contents:
# - ._*
# - .DS_Store
# - __MACOSX/
rsync -avz --filter='. .rsync-filter' source/ dest/
# Dry run before destructive sync
rsync -aHAX --dry-run --itemize-changes source/ dest/
Permissions, ACLs, and the macOS Permission Model
macOS uses POSIX permissions layered with NFSv4 ACLs, and the ACL system is more capable than Linux's POSIX ACLs. The `ls -le` flag shows ACLs inline with file listings, which is the fastest way to diagnose permission issues that POSIX bits alone do not explain.
The ACL entries are applied before the POSIX mode bits when macOS evaluates access. This means a file with `chmod 000` permissions can still be readable if an ACL grants read to the current user or their group. This behavior surprises Linux admins who expect `chmod 000` to be a hard block.
System Integrity Protection (SIP) is a separate mechanism from ACLs. SIP protects specific paths (`/System`, `/usr` excluding `/usr/local`, `/bin`, `/sbin`) at the kernel level. `csrutil status` tells you if SIP is enabled. You cannot modify SIP-protected paths even as root in normal operation. This is correct security behavior for endpoints, but it matters for scripts that try to write to `/usr/bin` or modify system Python installations.
For setting ACLs in deployment scripts, `chmod +a` is the macOS native syntax. For reading and manipulating them programmatically, `ls -le` combined with `chmod -a` for removal is the standard approach.
# Show POSIX permissions and ACLs
ls -le /path/to/file
# Add an ACL entry: give user read+execute on a directory
chmod +a "username allow read,execute" /path/to/dir
# Add group-level ACL
chmod +a "staff allow read" /path/to/shared
# Remove all ACLs from a file
chmod -N /path/to/file
# Remove specific ACL entry
chmod -a "username allow read,execute" /path/to/dir
# Propagate ACL recursively
chmod -R +a "developer allow read,write" /path/to/project
# Check SIP status
csrutil status
# List protected paths (informational, cannot modify)
ls -la /System/Library/
# Fix ownership recursively after misapplied chown
chown -R $(whoami):staff /path/to/project
Disk Usage, Storage Analysis, and APFS Space Accounting
APFS space reporting is confusing because of how it handles snapshots, purgeable space, and shared containers. `df -h` will show you total, used, and available, but the 'available' figure on macOS includes purgeable space - space used by APFS snapshots and caches that the OS can reclaim. This means `df` may show 40GB available when you can actually only write 5GB before hitting a real capacity limit.
The `diskutil info /` output gives you a breakdown that separates container free space, purgeable space, and file preallocation. For scripts that check disk space before large operations, use this rather than `df`.
For directory-level analysis, `du -sh` works as expected but is slow on large trees because it stats every file. `ncdu` from Homebrew is significantly faster and interactive. For scripted output, `du -sk * | sort -rn | head -20` gives the top 20 space consumers in the current directory sorted by size in kilobytes.
Time Machine local snapshots (APFS snapshots) consume what `df` shows as purgeable space. If your backup job is failing because a build script reports disk full while `df` shows gigabytes free, this is likely the cause. You can list and delete local snapshots with `tmutil`.
# Accurate disk space including purgeable breakdown
diskutil info / | grep -E 'Free Space|Purgeable'
# List Time Machine local snapshots
tmutil listlocalsnapshots /
# Delete a specific local snapshot
tmutil deletelocalsnapshots 2026-08-01-120000
# Delete all local snapshots (use with caution)
for snap in $(tmutil listlocalsnapshots / | grep 'com.apple'); do
tmutil deletelocalsnapshots "${snap##*.}"
done
# Fast directory size overview
du -sk * 2>/dev/null | sort -rn | head -20
# Human readable top-level usage
du -sh */ 2>/dev/null | sort -rh | head -20
# Install ncdu for interactive analysis
brew install ncdu
ncdu /
# Disk usage excluding Time Machine volume
du -sxh ~
# -x: do not cross filesystem boundaries
Automating File Operations in macOS: launchd and Shell Patterns
cron exists on macOS but Apple has deprecated it in favor of launchd for scheduled tasks. More accurately: cron works and is still present, but launchd is the right tool for system-level automation because it handles network availability, system sleep, and login/logout events that cron cannot.
For file management automation - rotating logs, syncing build artifacts, cleaning temp directories - a launchd plist with `StartCalendarInterval` or `StartInterval` is the production pattern. Plists live in `~/Library/LaunchAgents/` for user-level jobs or `/Library/LaunchDaemons/` for system-level jobs running as root.
When automating deployments that involve file operations across macOS endpoints at scale, teams increasingly use platforms like taskbotshub.ai for orchestrating multi-step workflows that combine shell commands, file transfers, and conditional logic without writing custom daemon code for every task. For one-off or development automation, shell scripts loaded via launchd remain the standard.
For project file organization specifically, consistent naming matters more than most teams acknowledge. If you are setting up file structures for projects that also involve domain registration or public-facing names, running your candidate names through a service like nicename.me to check availability and consistency across namespaces saves time before you commit directory structures and repo names that do not match your domain.
For the launchd plist itself, the critical fields are `Label`, `ProgramArguments`, and the scheduling key. Use `launchctl bootstrap` and `launchctl bootout` in macOS Monterey and later rather than the deprecated `load`/`unload` commands.
# Example launchd plist: clean build artifacts daily at 3am
# Save to ~/Library/LaunchAgents/org.myunix.cleanbuild.plist
cat > ~/Library/LaunchAgents/org.myunix.cleanbuild.plist << 'EOF'
Label
org.myunix.cleanbuild
ProgramArguments
/bin/sh
-c
find ~/builds -mtime +7 -delete
StartCalendarInterval
Hour
3
Minute
0
StandardOutPath
/tmp/cleanbuild.log
StandardErrorPath
/tmp/cleanbuild.err
EOF
# Load the agent (macOS Monterey+)
launchctl bootstrap gui/$(id -u) \
~/Library/LaunchAgents/org.myunix.cleanbuild.plist
# Check status
launchctl list | grep myunix
# Unload
launchctl bootout gui/$(id -u) \
~/Library/LaunchAgents/org.myunix.cleanbuild.plist
Spotlight, mdfind, and Metadata-Aware Search
macOS Spotlight indexes file metadata, content, and extended attributes, and you can query it from the terminal using `mdfind`. For sysadmins managing large file collections, `mdfind` is faster than `find` for content searches because it queries the pre-built index rather than walking the filesystem.
`mdfind` accepts boolean operators and metadata attribute queries using Spotlight query syntax. The `kMDItemKind`, `kMDItemFSSize`, `kMDItemContentModificationDate`, and `kMDItemDisplayName` attributes are the most useful for file management operations.
For cases where Spotlight is disabled (common on server installations or external volumes), fall back to `find`. You can check whether a path is indexed with `mdutil -s /path`.
`mdls` shows all Spotlight metadata for a specific file, which is useful for debugging why a file is or is not appearing in searches, and for inspecting what macOS has extracted from file content.
# Find all PDF files modified in the last 3 days
mdfind 'kMDItemContentTypeTree == "com.adobe.pdf" && kMDItemFSContentChangeDate >= $time.today(-3)'
# Search by filename
mdfind -name 'deployment.conf'
# Restrict search to a specific directory
mdfind -onlyin ~/Projects 'kMDItemDisplayName == "*.log"cdw'
# cdw = case insensitive, diacritic insensitive, word-based
# Show all metadata for a file
mdls /path/to/file.pdf
# Find large files via Spotlight
mdfind 'kMDItemFSSize > 1000000000'
# Check Spotlight indexing status for a volume
mdutil -s /
mdutil -s /Volumes/ExternalDrive
# Rebuild Spotlight index on a volume
sudo mdutil -E /Volumes/ExternalDrive
# Disable Spotlight on external drive (useful for backup volumes)
sudo mdutil -d /Volumes/BackupDrive
Secure File Operations: Wiping, Encryption, and Safe Deletion
The `srm` command (secure remove) was removed in macOS El Capitan (10.11) because Apple's engineering position is that it cannot reliably overwrite data on SSDs and APFS volumes where wear leveling and copy-on-write semantics mean the OS does not control which physical blocks are written. This is correct: on APFS, `rm` followed by `diskutil secureErase freespace` is the Apple-recommended approach for sensitive data removal.
For encrypting files on disk, macOS has built-in tools. `openssl` is present but linked to LibreSSL on modern macOS rather than OpenSSL - check `openssl version` before assuming behavior. For symmetric encryption of individual files, `openssl enc -aes-256-cbc -pbkdf2` is reliable. For disk-level encryption, FileVault 2 is APFS-native and managed via `fdesetup`.
For creating encrypted disk images that work like containers - useful for storing project secrets or sensitive configuration bundles - `hdiutil` creates APFS or HFS+ encrypted images with 128-bit or 256-bit AES.
# Encrypt a file with AES-256
openssl enc -aes-256-cbc -pbkdf2 -iter 100000 \
-in secrets.txt \
-out secrets.txt.enc
# Decrypt
openssl enc -aes-256-cbc -pbkdf2 -iter 100000 \
-d \
-in secrets.txt.enc \
-out secrets.txt
# Create encrypted disk image (256-bit AES, APFS)
hdiutil create \
-size 500m \
-fs APFS \
-encryption AES-256 \
-volname SecureVault \
~/SecureVault.dmg
# Mount encrypted image
hdiutil attach ~/SecureVault.dmg
# Unmount
hdiutil detach /Volumes/SecureVault
# Securely wipe free space (after deleting sensitive files)
# This is the Apple-recommended approach on SSD/APFS
diskutil secureErase freespace 0 /
# Level 0 = single pass zero, sufficient for SSD
# Check FileVault status
fdesetup status
# List FileVault-enabled users
fdesetup list