Opening Terminal and Understanding What You Are Running

Terminal.app lives at /System/Applications/Utilities/Terminal.app. Open it with Cmd+Space, type 'terminal', hit Enter. You land in zsh 5.9 (the version bundled with macOS 15 Sequoia). Run `echo $SHELL` to confirm. If you see /bin/zsh, you are on the default. If a previous admin installed bash via Homebrew and set it as the login shell, you might see /opt/homebrew/bin/bash instead.

The first thing to verify is your architecture. Apple Silicon Macs (M1, M2, M3, M4) use arm64. Intel Macs use x86_64. This matters for Homebrew paths and any compiled binary you install.

Run `uname -m` to check. On Apple Silicon you get 'arm64'. On Intel you get 'x86_64'. This single command will save you hours of debugging mismatched binary errors.

echo $SHELL
uname -m
sw_vers

BSD Userland vs GNU Coreutils: The Gotchas

This is where Linux admins hit walls. macOS ships BSD versions of core tools. `ls`, `grep`, `sed`, `awk`, `find`, `xargs` - all BSD variants. The flags are different, behavior differs, and scripts written for GNU tools will break.

The most common failure: `sed -i` for in-place editing. On GNU/Linux you run `sed -i 's/foo/bar/' file.txt` and it works. On macOS BSD sed, you need to provide an extension argument: `sed -i '' 's/foo/bar/' file.txt`. The empty string `''` is required. Leave it out and sed will error or create a backup file named with your substitution pattern.

Similarly, `date` on macOS uses BSD syntax. `date -d '2 days ago'` works on Linux. On macOS you need `date -v-2d`. For epoch conversion, Linux gives you `date -d @1700000000` but macOS needs `date -r 1700000000`.

The `find` command differences are subtler but bite you in scripts. GNU find accepts `-printf` for formatted output. BSD find does not. If you pipe find output through xargs and use `xargs -d ' '` that delimiter flag does not exist in BSD xargs either.

Our recommendation for anyone who runs scripts across both Linux and macOS: install GNU coreutils via Homebrew and prefix commands explicitly, or maintain separate script variants.

# BSD sed - note the empty string after -i
sed -i '' 's/old_string/new_string/' config.txt

# BSD date examples
date -v-2d          # 2 days ago
date -r 1700000000  # epoch to human readable

# Check if a tool is BSD or GNU
ls --version 2>&1 | head -1  # will error on BSD
grep --version | head -1

Installing Homebrew and Setting Up a Proper Toolchain

Homebrew is the de facto package manager for macOS. Install it with the official one-liner. On Apple Silicon, Homebrew installs to /opt/homebrew. On Intel Macs it goes to /usr/local. This path difference is important because any script that hardcodes /usr/local/bin/brew will fail on Apple Silicon.

After install, run `brew doctor` before installing anything else. It will flag missing Xcode Command Line Tools, conflicting paths, or permission issues. Fix those first. The Command Line Tools package gives you git, make, clang, and other essentials without requiring the full 12GB Xcode IDE.

To get GNU coreutils so your Linux muscle memory works: `brew install coreutils`. Homebrew installs the GNU versions with a 'g' prefix by default: `gls`, `gsed`, `ggrep`, `gfind`. If you want them as the default without the 'g' prefix, add the gnubin path to your PATH in ~/.zshrc.

For a proper sysadmin toolkit, install these in one shot. In our testing on macOS 15 with an M3 MacBook Pro, this base install takes under 4 minutes on a decent connection.

# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Run diagnostics first
brew doctor

# Install GNU coreutils and add to PATH
brew install coreutils
echo 'export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"' >> ~/.zshrc

# Sysadmin base toolkit
brew install wget curl git jq htop tmux vim ripgrep fd bat
// advertisement

Configuring zsh: The ~/.zshrc You Actually Need

macOS zsh reads ~/.zshrc for interactive shells and ~/.zprofile for login shells. This mirrors bash's ~/.bashrc and ~/.bash_profile split. A common mistake is putting PATH exports in ~/.zshrc only to find they are not available in scripts or SSH sessions - put PATH changes in ~/.zprofile or source ~/.zshrc from ~/.zprofile explicitly.

zsh has several quality-of-life features that bash lacks or handles differently. Globbing is more powerful in zsh. `**/*.log` expands recursively without needing `find`. Tab completion is context-aware out of the box with the right compinit call.

For a team that manages multiple macOS workstations, we keep a shared dotfiles repo and symlink configs. When you are naming that repo or any project directory, keeping the name clean and consistent matters more than it seems - something like 'dotfiles-macos' beats 'my-mac-stuff-v3-final'. The same principle applies if you ever register a domain for internal tooling documentation; services like nicename.me help you find clean, professional names that are not already taken.

Set HISTSIZE large. macOS default is 2000 lines. Set it to 50000 minimum. Also enable EXTENDED_HISTORY to record timestamps - invaluable when tracing what command caused an incident three days ago.

# ~/.zshrc - practical sysadmin config

# Homebrew path - Apple Silicon
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"

# GNU coreutils first
export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"

# History
HISTSIZE=50000
SAVEHIST=50000
HISTFILE=~/.zsh_history
setopt EXTENDED_HISTORY
setopt HIST_IGNORE_DUPS
setopt SHARE_HISTORY

# Better completion
autoload -Uz compinit && compinit
zstyle ':completion:*' menu select

# Useful aliases
alias ll='ls -lah'
alias grep='grep --color=auto'
alias ..='cd ..'
alias ...='cd ../..'

# Show git branch in prompt
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats ' (%b)'
setopt PROMPT_SUBST
PROMPT='%n@%m %~${vcs_info_msg_0_}$ '

File System Layout: Where Things Are on macOS

macOS uses a different directory hierarchy than Linux FHS, and several directories are hidden or locked down by System Integrity Protection (SIP) introduced in OS X El Capitan.

/usr/bin, /usr/sbin, /bin, /sbin are all protected by SIP. You cannot write to them even as root without disabling SIP, which you should not do on a production machine. This is why Homebrew puts everything in /opt/homebrew instead.

User-installed applications go to /Applications (all users) or ~/Applications (current user only). The equivalent of /etc for system config is /etc on macOS too, but most app-specific configs live in ~/Library/Preferences as .plist files, not flat text files. To read and write plist files from the terminal, use the `defaults` command or `plutil`.

The home directory structure includes ~/Library which is hidden in Finder by default. In Terminal it is fully accessible. ~/Library/Application Support holds app data, ~/Library/Logs holds app logs, ~/Library/LaunchAgents holds user-level launchd job definitions.

`/private/var` and `/private/tmp` are the real paths that /var and /tmp symlink to. Scripts that resolve symlinks will land in /private/var, which can cause confusion when comparing paths.

# Key paths to know
ls /opt/homebrew/bin      # Homebrew binaries (Apple Silicon)
ls /usr/local/bin         # Homebrew binaries (Intel) or manual installs
ls ~/Library/LaunchAgents # User launchd jobs
ls /Library/LaunchDaemons # System launchd jobs (root)

# Check if SIP is active
csrutil status

# Read a plist preference
defaults read com.apple.Terminal
defaults read -g AppleInterfaceStyle  # Dark/Light mode setting

Process Management: launchd Instead of systemd

macOS does not use systemd. It uses launchd, and the CLI tool for managing it is `launchctl`. If you manage Linux servers with systemctl, launchctl will feel foreign at first but the concepts map cleanly.

Launchd jobs are defined in XML plist files. System daemons live in /Library/LaunchDaemons and run as root. User agents live in ~/Library/LaunchAgents and run as the logged-in user. Load a job with `launchctl load`, start it with `launchctl start`, check status with `launchctl list`.

macOS 13 Monterey and later support a new `launchctl` syntax using domains. The legacy `load/unload` syntax still works but `launchctl bootstrap` and `launchctl bootout` are the current approach. Both syntaxes coexist in macOS 15.

For quick process inspection, `ps aux` works as expected. `top` is available but has different key bindings than Linux top - press 'o' to change sort order, 'q' to quit. `htop` from Homebrew behaves like the Linux version and is worth installing for interactive use.

For DevOps teams automating macOS build machines or CI runners, launchd job management is often abstracted by tools. If you are building automation pipelines that handle job scheduling across both Linux and macOS agents, platforms like taskbotshub.ai handle cross-platform job orchestration without requiring you to maintain separate systemd unit files and launchd plist files.

# List all running launchd jobs for current user
launchctl list

# Load and start a user agent
launchctl load ~/Library/LaunchAgents/com.myapp.agent.plist
launchctl start com.myapp.agent

# Check specific service status
launchctl list | grep com.myapp

# New-style bootstrap (macOS 13+)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.myapp.agent.plist
launchctl bootout gui/$(id -u)/com.myapp.agent

# Kill process by name (like killall on Linux)
killall -9 ProcessName
// advertisement

Networking Commands: What Changed From Linux

Network diagnostics on macOS use a mix of familiar and BSD-specific tools. `ping`, `traceroute`, `netstat`, `curl`, and `ssh` all work. But `ifconfig` is the primary interface tool - `ip addr` from the iproute2 suite does not exist natively.

`networksetup` is a macOS-specific CLI for managing network preferences. You can switch Wi-Fi on and off, list interfaces, set DNS servers, and configure proxies all from the terminal. This is the tool to reach for in scripts that need to modify network settings.

`scutil` is another macOS-specific command for querying system configuration. `scutil --nwi` shows network interface info. `scutil --dns` shows the current DNS configuration including search domains. `scutil --proxy` shows proxy settings.

Port scanning with `nmap` requires a Homebrew install. However, macOS includes `nc` (netcat) for quick connectivity tests. The macOS version of nc accepts `-z` for port scanning mode and `-v` for verbose output.

For checking what is listening on a port, Linux admins reach for `ss` or `netstat -tulpn`. On macOS use `lsof -i :PORT` or `netstat -an | grep LISTEN`. The `-p` flag on macOS netstat does not show PIDs - use lsof for that.

# List network interfaces
ifconfig
networksetup -listallnetworkservices

# Check what is listening on port 8080
lsof -i :8080

# DNS config
scutil --dns

# Quick port connectivity test
nc -zv hostname.example.com 443

# Show routing table
netstat -rn

# Flush DNS cache (macOS 12+)
dscacheutil -flushcache; sudo killall -HUP mDNSResponder

Clipboard, open, and pbcopy: macOS-Specific Utilities

macOS provides several command-line utilities that have no direct Linux equivalent and are genuinely useful once you know them.

`pbcopy` and `pbpaste` pipe data to and from the macOS clipboard. Pipe any command output to `pbcopy` and it lands in your clipboard ready to paste anywhere. This replaces the xclip or xsel workflow from Linux desktops.

The `open` command opens files, directories, and URLs using their associated application. `open .` opens the current directory in Finder. `open -a 'Visual Studio Code' .` opens the current directory in VS Code. `open https://myunix.org` opens the URL in your default browser. In shell scripts this replaces the `xdg-open` command from Linux.

`say` converts text to speech using macOS's built-in TTS engine. This is actually useful in long-running scripts as a completion notification: `make build && say 'build complete'`.

`caffeinate` prevents the Mac from sleeping. Run `caffeinate -t 3600` to keep it awake for one hour, or `caffeinate -i script.sh` to keep it awake for the duration of a command. On remote build machines or during large file transfers, this prevents sleep from interrupting the process.

`mdfind` is command-line Spotlight. It queries the Spotlight metadata index and is significantly faster than `find` for locating files by name or content on local volumes.

# Copy command output to clipboard
pwd | pbcopy
cat ~/.ssh/id_rsa.pub | pbcopy

# Paste clipboard content
pbpaste > output.txt

# Open current directory in Finder
open .

# Open URL in default browser
open https://myunix.org

# Prevent sleep during a long operation
caffeinate -i rsync -avz /source/ /destination/

# Find files using Spotlight index (fast)
mdfind -name 'httpd.conf'
mdfind 'kMDItemTextContent == "TODO"'

SSH Config and Key Management on macOS

SSH on macOS works like Linux but integrates with the macOS Keychain for passphrase storage. Instead of running ssh-agent manually and adding keys with ssh-add, macOS handles this through the Keychain if you configure it correctly.

The macOS-specific addition to ~/.ssh/config is `UseKeychain yes` combined with `AddKeysToAgent yes`. With this configuration, the first time you use a key, macOS prompts for the passphrase and stores it in Keychain. Subsequent connections use the stored passphrase without prompting.

For teams managing multiple SSH identities - personal GitHub, work GitHub, production servers, staging servers - the Host block pattern in ~/.ssh/config is essential. macOS SSH reads this file exactly like OpenSSH on Linux.

Key generation uses the same `ssh-keygen` command. For 2026, generate Ed25519 keys by default. RSA keys still work but Ed25519 are smaller, faster, and equally secure.

# Generate Ed25519 key
ssh-keygen -t ed25519 -C "admin@company.com" -f ~/.ssh/id_ed25519_work

# ~/.ssh/config - macOS with Keychain integration
Host github-work
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_work
  UseKeychain yes
  AddKeysToAgent yes

Host prod-*
  User admin
  IdentityFile ~/.ssh/id_ed25519_prod
  UseKeychain yes
  AddKeysToAgent yes
  StrictHostKeyChecking yes

# Add key to agent and Keychain
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_work
// advertisement

Permissions and sudo on macOS

macOS uses the same Unix permission model as Linux - owner/group/world with read/write/execute bits, plus ACLs. `chmod`, `chown`, and `chgrp` all work as expected. macOS extends this with NFSv4 ACLs that you can manage with `chmod +a` syntax or the `ls -le` flag to view.

sudo on macOS is configured in /etc/sudoers via `visudo`. The macOS default allows members of the 'admin' group to run any command with sudo. Check your group membership with `id`.

Gatekeeper is macOS's application signing enforcement. When you download a binary and try to execute it, Gatekeeper may block it. In the terminal, if you get 'operation not permitted' or 'cannot be opened because the developer cannot be verified', use `xattr -d com.apple.quarantine /path/to/binary` to remove the quarantine attribute. Alternatively, right-click in Finder and choose Open.

Transparency, Consent, and Control (TCC) is a separate permission layer. Even with sudo, Terminal may not have access to ~/Desktop, ~/Documents, ~/Downloads, your camera, or microphone unless explicitly granted in System Settings - Privacy & Security. If a script fails to read files in those locations despite correct Unix permissions, TCC is the reason. Grant Terminal full disk access in System Settings.

# View ACLs on a file
ls -le /path/to/file

# Remove quarantine attribute from downloaded binary
xattr -d com.apple.quarantine /usr/local/bin/somebinary

# Check all extended attributes
xattr -l /path/to/file

# Check your group membership
id

# Verify sudo access
sudo -l

# Check TCC database (requires full disk access)
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
  'SELECT client, auth_value FROM access WHERE service="kTCCServiceSystemPolicyAllFiles"'