# 12. macOS only; Linux and Windows support is removed, not deprecated

**Status:** Accepted · 2026-09-13

## Context

The package advertised three platforms. `README.md` said "macOS / Linux /
Windows", `lib/credential-store.sh` carried a `secret-tool` backend for Linux
and a PowerShell Credential Manager backend for Windows, and `test.yml` ran a
matrix over Ubuntu and macOS plus a dedicated `windows-test` job.

None of it was verified. ADR-0011 records that CI has not executed since
2026-07-02, and its own honest-status block already said the Linux path was
"coded, smoke-skipped (CI has no keyring agent)" and the Windows path "coded,
unverified this release". `docs/engineering.md` claimed "the CI matrix runs
macOS, Linux, and Windows", which stopped being true the day the runners went
away.

The rest of the pipeline never made the same promise. Every credential read
shells `security`. Every iOS build shells `xcodebuild`. Every piece of visual
evidence shells `simctl`. A run on another platform does not degrade into a
smaller run; it fails partway through, having already created a worktree and a
branch. Three advertised platforms, one that works.

## Decision

macOS is the only supported platform, and the package says so where a machine
can act on it:

- `package.json` declares `os: ["darwin"]`. npm enforces this on the root
  package - a non-macOS `npm ci` ends in `EBADPLATFORM` before a single file is
  written. This one field does more than deleting every branch would.
- `index.js` and `install/index.mjs` refuse a non-darwin host with one line
  naming the requirement. `uninstall`, `help` and `--version` are exempt:
  somebody who installed before this gate must still be able to remove it, and
  a person diagnosing a broken install needs the other two.
  `MULTI_AGENT_ALLOW_NON_DARWIN=1` overrides, and says on stderr that nothing
  on that path is tested.
- `doctor` gains `host-platform`, which runs first and blocks. It runs first so
  a non-macOS host gets one honest line instead of a cascade whose common cause
  is never stated.
- Every workflow runs on a macOS runner. `test.yml` loses its Ubuntu leg and
  its `windows-test` job, `pipeline/scripts/gate-linux.sh` and the
  `npm run gate:linux` script are deleted.

**The dead Linux and Windows code is removed separately, or not at all.** It
costs nothing at runtime - `detect_platform()` returns `macos` and the other
arms never execute - and removing it is where the real risk lives. Seven
constructs look like cross-platform scaffolding and are load-bearing on macOS:

1. `smoke-shell-portability.sh`'s `grep -P` ban. BSD grep has no `-P`, exits 2,
   and with `2>/dev/null` that reads as "found nothing". Two gates already
   shipped green on macOS for exactly this reason. The rule is _more_ relevant
   after this ADR, not less; only its header changes, from "supports macOS,
   Linux and Windows" to "the runtime is BSD userland and bash 3.2".
2. The `sha256sum || shasum` ordering in seven scripts. The gate fails a file
   that names either one alone, and macOS ships `shasum` while a mac with brew
   coreutils has both. Net change: zero lines.
3. `credential-store.sh`'s node reader for `keychainMapping`. Its comment
   blames Windows; its condition is `command -v node`, so it fires on any host
   without `python3`, macOS included.
4. `delegate_to_python()` and `do_set`'s `[ "$PLATFORM" != "macos" ] &&` guard.
   Together they are what keeps macOS writes on `security -i`, with the secret
   on stdin and never on argv. Simplifying one without the other silently moves
   the write onto the delegate.
5. `github-ssh-setup.sh`'s Darwin arm. `UseKeychain yes` _is_ the macOS
   behaviour; the other arm is an empty string.
6. The `stat -c ... || stat -f ...` chains. GNU-first ordering is required
   because `stat -f` is a valid GNU flag (`--file-system`) that succeeds.
7. `credential-store-resolver.sh`, which has no OS content at all - it resolves
   Claude / Copilot / Codex install trees.

## Consequences

- A Linux or Windows user cannot install. That is the point: the alternative
  was letting them install and fail at Phase 3.
- `rollup.byOs` in the usage panel keeps its key. A row with the field missing
  should read as "unknown host", not as "macOS".
- `test.yml` is still dormant per ADR-0011. What it would verify on waking is
  now a clean macOS runner and a lockfile install, not a platform matrix.
- The `os` field is a hard stop with no per-file discipline required, which is
  why it is listed first. Deleting 126 lines of unreachable branches buys no
  behaviour and risks the seven items above; it is optional cleanup, gated
  behind a green `npm test` on both sides of each removal.

## Alternatives

**Deprecate rather than remove.** A deprecation window helps users who have a
working install. Nobody has one: the paths were never verified and CI has not
run since July.

**Keep the code, drop only the claim.** Tempting, and it is what the optional
part of this decision does. But leaving `secret-tool` in the tree with no
promise attached means the next reader has to work out whether it is supported,
which is the ambiguity the `os` field removes in one line.

**Self-hosted macOS runner.** Considered and rejected in ADR-0011 for a public
repository: a self-hosted runner executes code from any fork's pull request.
