Claude on WholeTechnetwork
home/settings/os-matrix
Windows · macOS · Linux · WSL

The OS matrix

Claude Code runs the same way on every OS — until it doesn't. This page is the catalogue of the places it doesn't: paths, line endings, junctions vs symlinks, sandboxes, schedulers, antivirus, default shells, and the one-config-fits-all tricks that mostly hold up across Windows, macOS, Linux, and WSL.

Pair with /settings/terminals for the shell-specific layer beneath this.

01 — Matrix

Cross-OS at a glance

BehaviourWindowsmacOSLinuxWSL
Config homeC:\Users\<u>\.claude\~/.claude/~/.claude//home/<u>/.claude/
Default shellPowerShell 7zsh (since Catalina)bash (distro default)bash (or whatever the WSL distro ships)
Default installnpm i -g @anthropic-ai/claude-codebrew install claude-codenpm i -g … or distro pkgSame as Linux distro
SandboxPath-based only (no kernel sandbox)Seatbelt (sandbox-exec)Landlock + seccompLinux sandbox applies
Symlinks need admin?Yes by default (or Developer Mode)NoNoNo
Symlink alternativeNTFS junction (mklink /J)
Line endingsCRLF (mostly)LFLFLF
Case-sensitive FSNo (NTFS default)No (APFS default)Yes (ext4 etc.)Yes (in WSL)
SchedulerTask Scheduler / schtaskslaunchd / launchctlcron / systemd timersLinux cron (only fires when WSL is running)
Antivirus interferes?Defender scans some hooksRare (Gatekeeper signs)RareRare (Defender doesn't read into WSL by default)
The shared core: across all four OSes, settings.json, CLAUDE.md, commands/, agents/, and skills/ are all identical files. The differences are in how the surrounding environment treats them. Sync the files via git; deal with the environment differences once per OS.
02 — Windows

Windows specifics

PowerShell 7 (sometimes called pwsh) is the modern Windows shell Claude Code defaults to in interactive use, not the older powershell.exe (5.1) or cmd.exe. The bash tool in Claude Code calls out to Git Bash if it's installed, otherwise WSL bash, otherwise falls back to PowerShell — keep an eye on which one your hooks expect.

What's unusual

Cheats

# open WSL bash for a single Claude command from PowerShell
PS> wsl bash -c "claude --print 'summarise CHANGELOG.md'"

# junction (preferred over symlink for directories)
PS> mklink /J "$env:USERPROFILE\.claude\projects\foo\memory" "$env:USERPROFILE\OneDrive\claude-memory\foo"

# run a scheduled Claude task daily at 09:00
PS> schtasks /create /tn "Claude dotfiles pull" /tr "git -C C:\Users\walhu\.claude pull --ff-only" /sc daily /st 09:00
The NUL trap: bash 2>/dev/null doesn't translate to PowerShell 2>$null. If you write hooks in JSON and want them portable, prefer 2>NUL (which works in both via cmd-shell semantics) or branch on $IsWindows in PowerShell hooks.
03 — macOS

macOS specifics

The cleanest desktop for Claude Code: real symlinks, a kernel-level sandbox (Seatbelt), zsh as the default shell, and Homebrew as a coherent package manager. Most things "just work."

What's unusual

Cheats

# run a daily Claude job via launchd (preferred over cron on Mac)
$ cat > ~/Library/LaunchAgents/com.walhus.claude-pull.plist <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<plist><dict>
  <key>Label</key><string>com.walhus.claude-pull</string>
  <key>ProgramArguments</key><array>
    <string>/usr/bin/git</string>
    <string>-C</string><string>/Users/walhu/.claude</string>
    <string>pull</string><string>--ff-only</string>
  </array>
  <key>StartCalendarInterval</key><dict><key>Hour</key><integer>9</integer></dict>
</dict></plist>
XML
$ launchctl load ~/Library/LaunchAgents/com.walhus.claude-pull.plist
04 — Linux

Linux specifics

The native habitat for Claude's Bash tool — and the OS your droplet, NAS, and most VPSes run. Differences across distros (Ubuntu, Debian, Fedora, Arch, Alpine) are mostly about package managers; the rest is the same.

What's unusual

Cheats

# cron entry for nightly Claude task on the droplet
$ crontab -e
0 3 * * * /usr/local/bin/claude --print "scan today's /var/log/nginx/error.log for unusual patterns" > /var/log/claude-nightly.md 2>&1

# systemd timer (preferred over cron on modern distros)
$ systemctl --user edit --force --full claude-pull.service
# [Service]
# ExecStart=/usr/bin/git -C %h/.claude pull --ff-only
$ systemctl --user edit --force --full claude-pull.timer
# [Timer]
# OnCalendar=daily
# Persistent=true
05 — WSL

WSL — Linux Claude inside Windows

Windows Subsystem for Linux (WSL2) is a real Linux kernel running alongside Windows. Run Claude Code inside it and you get the Linux sandbox, Linux paths, Linux tools — but you can still drag files in from Windows Explorer. For mixed-OS fleets it's often the cleanest answer to "do I want one Windows config or one Linux config?"

What's unusual

The hybrid pattern that works

Many WholeTech-style operators run Claude Code inside WSL on their Windows boxes — they get the Linux sandbox and the parity with the droplet. Windows is the OS layer; WSL is the development environment. The Windows-side ~/.claude/ exists but is largely unused; the WSL-side one is where the config lives.

One-line bash from PowerShell: wsl bash -lc "<your command>". The -l loads your bash profile so your aliases work. Useful inside Windows scheduled tasks that want to fire a Linux Claude job.
06 — Paths

Path translation across OSes

Hardcoded paths in settings.json are the most common cross-OS friction. Three coping strategies, in order of preference:

1. Use ${env:HOME} / $env:USERPROFILE

"mcpServers": {
  "fs-home": {
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-filesystem", "${env:HOME}/projects"]
  }
}

Claude Code expands env vars in settings.json. HOME works on macOS/Linux/WSL; on Windows, USERPROFILE is the equivalent. JSON config rarely needs to know the OS — just the variable.

2. Forward slashes everywhere

Windows accepts forward slashes in most contexts (PowerShell, Node, Python, Go). So C:/Users/walhu/websites works in JSON as a path on Windows even though convention is backslashes. Saves you from \\ JSON-escape hell.

3. Per-OS overrides

Some keys really do differ per OS. Keep a base settings.json in git and a per-OS overlay settings.<os>.json applied during bootstrap.

# in your bootstrap script
case "$(uname)" in
  Darwin) cp settings.macos.json ~/.claude/settings.json ;;
  Linux)  cp settings.linux.json ~/.claude/settings.json ;;
esac
07 — Line endings

CRLF vs LF

The classic cross-OS gotcha. JSON parsers tolerate both. Shell scripts in hooks do not — a CRLF-terminated bash script throws a confusing "command not found" because the interpreter's name has an invisible \r on the end.

The git settings that handle it

# in ~/.claude/.gitattributes
*.sh           text eol=lf
*.json         text eol=lf
*.md           text eol=lf
*.ps1          text eol=crlf
hooks/**       text eol=lf

This forces hook scripts to LF regardless of OS. PowerShell scripts (which you'll only ever run on Windows) can stay CRLF.

The symptom to recognise: a hook fires on macOS/Linux fine but on Windows you get '\r': command not found or : command not found with an invisible character. CRLF in the script. Fix the .gitattributes, re-checkout the files.
09 — Sandbox & AV

Sandboxes & antivirus

OSSandboxAntivirus concerns
WindowsPath-based only (no kernel sandbox).Defender real-time scan can slow hooks. Add ~/.claude/ as an exclusion if you see 100ms+ hook delays.
macOSSeatbelt via sandbox-exec — kernel-level, fast, denies writes outside workspace.Mostly invisible. Gatekeeper signs the install; don't sideload from elsewhere.
LinuxLandlock + seccomp — kernel-level, strictest.ClamAV (if you run it) is fine. SELinux/AppArmor occasionally interfere on Fedora/Ubuntu hardened.
WSLLinux sandbox applies inside WSL.Defender doesn't scan into WSL by default.

Practical: if a hook works locally on one machine and dies on another with a permission error, sandbox or AV is the first suspect. Run the hook command directly in a shell on the failing machine — if it works there but fails through Claude, sandbox; if it fails everywhere, real permission issue.

10 — Permissions

Permission patterns per shell

The Bash(...) permission pattern matches against the full command string Claude runs. The string differs between shells, so an allowlist entry that works on Mac/Linux may not match on Windows.

Command intentmacOS / Linux / WSLWindows (PowerShell)
git statusBash(git status:*)Bash(git status:*)
cURL a URLBash(curl:*)Bash(curl.exe:*)
DNS lookupBash(dig:*) / Bash(host:*)Bash(nslookup:*) / Bash(Resolve-DnsName:*)
Remove a file (safe)Bash(rm:*)Bash(Remove-Item:*)
List filesBash(ls:*)Bash(Get-ChildItem:*) / Bash(ls:*) (PowerShell aliases ls)

For a cross-OS allowlist, list both forms. The allow list is OR-matched — having extra entries doesn't hurt.

"permissions": {
  "allow": [
    "Bash(curl:*)",     // macOS/Linux
    "Bash(curl.exe:*)", // Windows
    "Bash(dig:*)",
    "Bash(nslookup:*)"
  ]
}
11 — Case sensitivity

Filesystem case sensitivity

macOS APFS and Windows NTFS are case-preserving but case-insensitive. Linux ext4 is both. A file named README.md on Mac is also reachable as readme.md; on Linux it isn't.

What this breaks

The rule

Pick a case convention and stick to it — lowercase for everything is the safe default. Add core.ignoreCase = false in your repo so git surfaces case-only renames as conflicts you must resolve, not silently merges them.

12 — Scheduler

Scheduling Claude work, per OS

Windows — Task Scheduler / schtasks

PS> schtasks /create /tn "Claude pull" /tr "git -C C:\Users\walhu\.claude pull --ff-only" /sc daily /st 09:00

macOS — launchd

See the macOS section for a full .plist example. launchctl load to activate; launchctl unload to disable; launchctl list | grep claude to inspect.

Linux — cron or systemd timers

# cron
$ crontab -e
0 9 * * * /usr/bin/git -C $HOME/.claude pull --ff-only

# systemd timer (better, has logs)
$ systemctl --user enable --now claude-pull.timer

WSL — Linux cron, with a catch

Cron inside WSL only runs while WSL is running. If WSL isn't always on, schedule from the Windows side instead — a Windows scheduled task that invokes wsl bash -lc "...".

The cross-OS standard for scheduled Claude work: on Linux/macOS, use the native daemon. On Windows, use Task Scheduler and either call PowerShell directly or invoke into WSL. Don't try to make cron run on Windows — it's a fight you don't need.
13 — Choosing

Picking a primary OS

If you're starting fresh or consolidating, here's a cheat sheet for which OS to centre your Claude work on, by who you are.

solo dev

macOS

Cleanest desktop, real symlinks, Seatbelt sandbox, brew. The path of least resistance if you can choose.

windows-first

Windows + WSL

Run Claude in WSL Ubuntu. Get the Linux sandbox, the parity with your droplet, and keep Windows for the desktop apps you need.

infra-heavy

Linux

If your work is mostly server-side (droplet config, CI, deploys), running Claude on Linux means your local environment matches production.

The fleet doesn't need to be uniform. Plenty of WholeTech-shaped setups run macOS at home, Windows at the shop, Linux on the droplet, and WSL on the work laptop — and they all sync the same ~/.claude/ via git. The matrix above is how you keep them from stepping on each other.

Live
◐ Theme