Every place tracked source depends on macOS, grouped by seam class, with the portable path that already exists or the limit that stands in for one. A seam with no Linux or Windows path is a stated limit, not a gap that reads as support.
node bin/lib/platform-seams.mjs . --filesPopulation: tracked .mjs .cjs .js .sh .bash .zsh .py .ts files outside test/ directories (758
on 2026-10-08, all read). Whole-line comments are skipped, so a hit is code or a string literal
rather than prose about a seam; users-path is the exception and counts comments too, because a
home path in a comment discloses as much as one in code. A zsh line that also names bash, or a
macos-tools line that also names python, perl, ruby or node, is an interpreter name list
used for classification and is not counted. The regex per class is in bin/lib/platform-seams.mjs
(SEAM_CLASSES).
Measured 2026-10-08:
| Class | Files | Lines | What it matches |
|---|---|---|---|
launchd |
10 | 41 | launchctl, LaunchAgents/LaunchDaemons, plist schedule keys |
keychain |
7 | 14 | spawning security, security find-/add-/dump-…, keychain: refs, osxkeychain |
zsh |
1 | 2 | zsh shebangs, 'zsh' literals, .zshrc/.zprofile |
users-path |
3 | 3 | /Users/<name> |
homebrew |
21 | 47 | /opt/homebrew, Cellar, brew <subcommand>, 'brew' literals |
sandbox-exec |
1 | 3 | sandbox-exec |
macos-tools |
10 | 37 | sw_vers osascript xcrun codesign notarytool spctl plutil pbcopy pbpaste system_profiler softwareupdate mdfind tccutil, spawning open, 'ditto'/'xattr' literals, BSD sed -i '', stat -f, date -j |
bin/test/platform-seams.test.mjs re-measures on every suite run and fails when a class appears
in a file the allow-list does not name, or when a listed file no longer has
the seam. Line counts inside a listed file are not pinned: they move with ordinary edits.
This document lists macOS seams by reading the source. Windows support comes from what CI measured there, and it is partial:
Windows CI measured 2026-10-07 at b6a3167c, Node 24.21.0: 233 pass / 0 fail / 1 skipped.
The run covered the 16 platform-contract test files the windows-latest job names. The one skip
has a stated reason: a #!/bin/sh shim named git is not executable on Windows. CI records no
Windows pass or fail count for the rest of the suite. The bin/ and lib/ tests run there only to
check that the tree stays clean. The admin/, monitor/, cra/, map/ and sitemap/ tests,
apart from the two monitor/ port-bind files, do not run there at all. The Node 22 leg, added on
2026-10-08, has completed and passed since; no Node 22 totals are recorded here. The full statement
is in SCOPE.md, and node bin/platform-support.mjs --check holds both
documents to the recorded run.
Linux has no seam of its own in the table below; what it lacks is listed there as each macOS
seam's limit. It has two install paths, both described with their limits in
container/README.md. bin/install.sh is POSIX sh and installs a
checkout's HEAD or a release tarball checked against its SHA-256. container/Dockerfile builds a
Debian 12 image with bwrap, pasta and seven scanners, and sets CW_SANDBOX=require. Without
--security-opt seccomp=unconfined its host lanes are refused as noscan rather than run
unconfined, and without --device /dev/net/tun so are the lanes with network egress. The image
has been built and run on Docker Desktop for arm64. .github/workflows/container.yml builds it on
an amd64 runner when its inputs change; its three runs so far, all on 2026-10-08, failed.
| Files | What it does | Portable path or limit |
|---|---|---|
monitor/install-agents.mjs |
Generates and loads the per-area sweep, liveness and watch agents (plists, launchctl bootstrap/bootout/print) |
Limit. Refuses on non-darwin and says to run the same commands from systemd user timers or cron. No unit generator exists. Portable alternative: a systemd --user .service + .timer pair per agent with OnCalendar= taken from the same schedule, or one crontab line per agent. For example, the daily evidence refresh (com.portll.commitwork-cra-refresh, 06:30) maps to OnCalendar=*-*-* 06:30 or 30 6 * * * node cra/refresh.mjs; its freshness reader (cra/evidence-status.mjs) is portable and reports the pack stale whichever scheduler stops. The daily docsite publish (com.portll.commitwork-docsite-publish, 07:00) maps to OnCalendar=*-*-* 07:00 or 0 7 * * * node bin/docsite-publish-scheduled.mjs, with CW_WRANGLER, CW_DOCSITE_PROJECT and CW_CLOUDFLARE_ZONE_ID set in the unit's Environment= or the crontab; the script itself is portable and --status reads its ledger under any scheduler. The daily digest (com.portll.commitwork-digest) starts an hour after the last nightly sweep slot, 03:30 with one area, and maps to OnCalendar=*-*-* 03:30 or 30 3 * * * node bin/digest-scheduled.mjs, moved by 15 minutes for each further nightly area. Its delivery URLs are credentials: give them through the unit's EnvironmentFile= or a file: ref in the secrets table, never in the unit or crontab text. The script is portable, records each run in <reports>/scheduled-job-journal.jsonl for the next digest to read, and --status prints that journal under any scheduler |
monitor/service-health.mjs, bin/agent-surface.mjs, admin/static/panel-settings.js |
Ask launchctl print gui/<uid>/<label> whether a job is loaded; the panel shows launchctl in place of a toggle for launch programs |
service-health already records a failed or missing launchctl as null (unknown), never as "not loaded". Portable alternative: systemctl --user is-active <unit> |
admin/routes/rollups.mjs, bin/adjudication-sampler.mjs |
Read schedules from installed plists: when each area runs next, and the liveness cadence the deadman check measures lateness against | Env-overridable (CW_AGENT_DIR, CW_LIVENESS_PLIST). A missing plist reads as absent or lateness undefinable, never on time. On Linux both stay that way until schedules are read from timers |
admin/lib/spine-preconditions.mjs |
Checks the supervisor plist for the overwatch layer | Already returns not-applicable off darwin; CW_LAUNCH_AGENTS_DIR overrides the path |
bin/panel-local-name.mjs |
Writes install scripts for commitwork.local using a pf anchor and a LaunchDaemon |
Limit: macOS only. Portable alternative: an nftables/iptables redirect, or setcap cap_net_bind_service on the panel's node |
monitor/persistence-diff.mjs |
Watches LaunchAgents/LaunchDaemons as persistence surfaces | Portable already: a Linux surface set (systemd units, cron, profiles) is selected by platform |
bin/deploy.mjs |
Lists ~/Library/LaunchAgents among paths it refuses to write |
Protective, not a dependency. The same list carries /etc/cloudflared for Linux |
bin/hooks/guard-destructive.mjs |
Names launchctl and plists in the pattern that refuses an attempt to switch the human-commit gate off |
Protective, not a dependency. The same pattern names .zshrc and .profile |
| Files | What it does | Portable path or limit |
|---|---|---|
lib/secrets.mjs |
Resolves keychain:<service>/<account> refs with security find-generic-password |
Portable already: the file:<absolute path>#<key> backend works on every platform, and a keychain ref off darwin returns unsupported-platform instead of an empty value |
bin/secrets.mjs, bin/addsecret.mjs |
set stores a value with security add-generic-password |
set refuses off darwin; secrets ref NAME file:… records a file-backed ref on any platform. A libsecret (secret-tool) or pass backend would be a third REF_RE form |
monitor/archive-batches.mjs |
Keeps the archive encryption key in the login keychain | Limit: macOS only. No fallback is read. Portable alternative: resolve the key through lib/secrets.mjs so a file: ref works |
monitor/credential-scope.mjs |
Parses security dump-keychain to inventory stored items |
Returns null off darwin (unknown, not empty); CW_CRED_DUMP supplies a fixture |
monitor/images.mjs |
Names docker-credential-osxkeychain in a fix hint |
Portable already: the hint is chosen per platform, with a Linux line for secretservice/pass |
monitor/journey.mjs |
Names keychain:<service>/<account> as the example reference in the optional credentials step's CLI hint |
Portable already: the step is optional and lib/secrets.mjs also resolves file: references; only the example text names the keychain |
No tracked script needs zsh. Of the 21 tracked .sh files, 20 start #!/usr/bin/env bash (13),
#!/bin/bash (2) or #!/bin/sh (5), and bin/lib/docker-config.sh is sourced and declares
shell=bash. Two files are counted. monitor/persistence-diff.mjs watches .zshrc and
.zprofile as persistence surfaces beside .bashrc and .profile, and
bin/hooks/guard-destructive.mjs names .zshrc in the pattern that refuses an attempt to switch the
human-commit gate off. Both are observation, not a dependency. Interpreter name lists that include
zsh beside bash (bin/agent-config.mjs, monitor/process-ancestry.mjs) are not counted.
No tracked file names a real home directory. Every hit in source is a placeholder or a detector:
bin/anchor-staleness.mjs (HOME_PLACEHOLDER = '/Users/username/', the redaction target) and
lib/launchlist-checks.mjs (the regex that finds home paths for the launch checklist). Outside source the same holds:
tests keep a home-shaped path only where the code under test is about home directories, with
synthetic names (x, u, op, user, someone, operator, a, b-c.d), and put repository,
report and tool paths outside any home (/work, /srv, /opt, /tmp). Lane-capability fixtures carry
/src/fixture for the home directory of the run that produced them. monitor/failure-taxonomy.json,
docsite/*/taxonomy-reference.html and provenance/upstream-sources.chain.jsonl carry
/Users/username/…, the redacted form. Re-run with
git grep -I -o -h -E '/Users/[A-Za-z0-9_.-]+' | sort | uniq -c. Nothing needed an
os.homedir() fix. The launch checklist's absolutePaths check is the gate that keeps it so.
| Files | What it does | Portable path or limit |
|---|---|---|
bin/setup.mjs, admin/routes/scanners.mjs, lib/launchlist-checks.mjs |
Treat brew as one installer among several; the launch checklist queries it as one package registry beside npm, crates, PyPI and Docker | Portable already: setup tries brew pipx npm cargo gem composer go, plus release apt dnf on Linux |
bin/commitwork.mjs, bin/races.mjs, bin/tls-headers-scan.mjs, bin/yara-witness.mjs, bin/scorecard-scan.sh, lib/release-signing.mjs |
brew install X in a not-installed message |
Text only. The tool is found on PATH whatever installed it; the cosign message also names winget and scoop |
monitor/package-inventory.mjs, monitor/update-vulns.mjs, monitor/app-inventory.mjs, admin/routes/packages.mjs |
Inventory and upgrade lanes for brew formulae and casks | A missing brew is unavailable/absent, never "up to date". Limit: no apt/dnf counterpart lane |
monitor/install-agents.mjs, monitor/detection-reducer.mjs, sitemap/harvest.mjs, bin/hermetic-test.mjs |
/opt/homebrew/... in binary candidate lists |
install-agents is darwin-only (above). The other three also list /usr/local or Linux paths, or take an env override |
admin/routes/journey.mjs |
/opt/homebrew/bin among the directories the guided setup searches for installed tools |
Portable already: the same list holds ~/.local/bin, ~/.cargo/bin, ~/go/bin and /usr/local/bin, so a Linux install is found without it |
bin/lib/sandbox.mjs |
/opt/homebrew in the macOS read allow-list |
Used only in the darwin profile |
bin/deploy.mjs |
/opt/homebrew/etc/cloudflared among refused write paths |
Protective; /etc/cloudflared is listed beside it |
joernwork/bin/fix-local-joern.sh |
Finds joern via brew --prefix |
JOERN_PREFIX overrides it |
monitor/images.mjs |
Brew in the macOS fix hint | Per-platform hint (see Keychain) |
| Files | What it does | Portable path or limit |
|---|---|---|
bin/lib/sandbox.mjs |
Builds the macOS Seatbelt profile that confines scanner lanes | Portable on Linux: the same entry point builds a bwrap argv. Limit: any other platform throws no host sandbox for platform, and the probe reports available: false |
| Files | What it does | Portable path or limit |
|---|---|---|
monitor/update-vulns.mjs, monitor/package-inventory.mjs, admin/routes/packages.mjs |
OS update lane: sw_vers, softwareupdate, plutil |
Limit: macOS only. A missing tool is tool-failed/unavailable, never clean. No Linux distro-advisory lane exists |
monitor/app-inventory.mjs |
App bundles: plutil, codesign, xattr quarantine |
Limit: macOS surfaces only. A missing tool marks the surface absent |
admin/routes/remediation.mjs |
Opens VS Code with open -a and Terminal via osascript for a hand-off |
CW_OPEN/CW_OSASCRIPT override the binaries. Portable alternative: code <dir> and the overwatch runner, which the route tries first |
bin/daily-run.mjs, monitor/images.mjs |
Desktop notification or popup through osascript |
Best-effort. images reports osascript not found as such. Portable alternative: notify-send |
bin/md-view.mjs |
Opens a rendered page with open |
Portable already: explorer.exe on win32, xdg-open elsewhere |
bin/codeql-swift-build.sh |
Runs /usr/bin/xcrun swift build for CodeQL's Swift extractor |
Limit: macOS only, as CodeQL's Swift support is |
bin/panel-local-name.mjs |
BSD sed -i '' in the generated uninstall script |
Part of a macOS-only tool (see launchd) |
The files each class may appear in. The test parses this table; add a row in the same change that adds a seam, and say in the sections above what the portable path is.
| Class | File |
|---|---|
| launchd | admin/lib/spine-preconditions.mjs |
| launchd | admin/routes/rollups.mjs |
| launchd | admin/static/panel-settings.js |
| launchd | bin/adjudication-sampler.mjs |
| launchd | bin/agent-surface.mjs |
| launchd | bin/deploy.mjs |
| launchd | bin/hooks/guard-destructive.mjs |
| launchd | bin/panel-local-name.mjs |
| launchd | monitor/install-agents.mjs |
| launchd | monitor/persistence-diff.mjs |
| launchd | monitor/service-health.mjs |
| keychain | bin/addsecret.mjs |
| keychain | bin/secrets.mjs |
| keychain | lib/secrets.mjs |
| keychain | monitor/archive-batches.mjs |
| keychain | monitor/credential-scope.mjs |
| keychain | monitor/images.mjs |
| keychain | monitor/journey.mjs |
| zsh | bin/hooks/guard-destructive.mjs |
| zsh | monitor/persistence-diff.mjs |
| users-path | bin/anchor-staleness.mjs |
| users-path | lib/launchlist-checks.mjs |
| homebrew | admin/routes/journey.mjs |
| homebrew | admin/routes/packages.mjs |
| homebrew | admin/routes/scanners.mjs |
| homebrew | bin/commitwork.mjs |
| homebrew | bin/deploy.mjs |
| homebrew | bin/hermetic-test.mjs |
| homebrew | bin/lib/sandbox.mjs |
| homebrew | bin/races.mjs |
| homebrew | bin/scorecard-scan.sh |
| homebrew | bin/setup.mjs |
| homebrew | bin/tls-headers-scan.mjs |
| homebrew | bin/yara-witness.mjs |
| homebrew | joernwork/bin/fix-local-joern.sh |
| homebrew | lib/launchlist-checks.mjs |
| homebrew | lib/release-signing.mjs |
| homebrew | monitor/app-inventory.mjs |
| homebrew | monitor/detection-reducer.mjs |
| homebrew | monitor/images.mjs |
| homebrew | monitor/install-agents.mjs |
| homebrew | monitor/package-inventory.mjs |
| homebrew | monitor/update-vulns.mjs |
| homebrew | sitemap/harvest.mjs |
| sandbox-exec | bin/lib/sandbox.mjs |
| macos-tools | admin/routes/packages.mjs |
| macos-tools | admin/routes/remediation.mjs |
| macos-tools | bin/codeql-swift-build.sh |
| macos-tools | bin/daily-run.mjs |
| macos-tools | bin/md-view.mjs |
| macos-tools | bin/panel-local-name.mjs |
| macos-tools | monitor/app-inventory.mjs |
| macos-tools | monitor/images.mjs |
| macos-tools | monitor/package-inventory.mjs |
| macos-tools | monitor/update-vulns.mjs |