Repository navigation
Conversation
- CryptoKit is Apple-only; swift-crypto's Crypto re-exports it on Darwin and builds BoringSSL only off-Darwin, so one import serves both - Pinned known-answer digests guard the seam that .mcs-project state depends on Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- OSAllocatedUnfairLock is Darwin-only and Synchronization.Mutex is macOS 15+, above this package's floor, so an NSLock-backed box serves both call sites - Raw-mode pickers treat a hangup as No and leave a warning in scrollback; readByte takes the descriptor so the EOF exit is covered by a pipe test Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- termios c_cc is indexed through the platform's VMIN/VTIME rather than literals - The PTY bridge's poll decisions move to a pure PTYBridge so the /dev/null-stdin spin and drain-before-drop are table-tested; O_CLOEXEC stops a child holding the process lock after mcs exits - A bridge that fails before exec now reports why instead of "see output above" Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- Linuxbrew's prefixes, and advice that names the system package manager where brew cannot act, so no hint points back at a command with no exit - ensureClaudeCLI never offers a cask Linuxbrew does not have, and takes injectable brew/confirm seams so both macOS branches are asserted Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- NSHomeDirectory() reads the passwd entry and ignores $HOME on Darwin too, so HOME=... mcs ... was silently ignored everywhere; user-typed ~ now expands against the same home as ~/.mcs - brewPrefix resolves symlinks then strips a trailing Homebrew component, so a shim and the installer's own layout both yield the prefix formulae link into Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- brew upgrade only exists where mcs came from the tap; Linux gets a staged, version-verified tarball swap at the path /proc/self/exe resolves - With nothing runnable the hook names the releases page and asks no question, instead of emitting an empty "On yes, run:" Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- PR checks and the release build run on ubuntu-22.04 and ubuntu-22.04-arm, so x86_64 and aarch64 ship from one release with one SHA256SUMS - One composite action installs the toolchain for every Linux job - A manual Linux Smoke workflow exercises whole command flows against a real pack; it is what the compatibility matrix rests on Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- docs/linux-support.md records the supported configurations, the compatibility matrix and an ADR per platform decision; telemetry and the lockfile are gone from main, so neither needs a Linux entry - CLAUDE.md, README and the docs name both architectures and the six files allowed to hold platform-dependent values Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- Ubuntu 22.04 needs pkg-config and python3-lldb-13 on top of libcurl's headers, and the names differ by release and architecture - Read them back from swiftly's own error instead of hardcoding a list Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- SwiftFormat and SwiftLint both publish static Linux binaries, so linting needs no Swift toolchain and no macOS runner; the newest release is still what runs - Quote the release tarball globs so a name with a leading dash cannot become an option (actionlint/shellcheck SC2035) Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- The unzip directory and the install destination shared a name, so install resolved source and target to the same file and exited 1 Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- fileExists(atPath:isDirectory:) is @discardableResult only on Darwin, so discarding it fails the Linux build under -warnings-as-errors - Gating on the return value is also what Apple documents: isDirectory is undefined when the path does not exist Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- Each Linux job downloaded 959 MB and spent ~50s installing the toolchain; a cache keyed on version and architecture skips that on a hit - The system packages stay outside the cache, since a cached toolchain needs them too; swiftly's own list remains the fallback on a miss - The action now reports the resolved Swift version, which also proves a cache hit is usable before anything builds Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- .build/checkouts and the resolver state are git clones with no absolute paths, so a per-runner key stored the same bytes six times and made five jobs miss - Compiled output stays uncached: it is not portable across runner or toolchain Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- A new upstream release could turn a PR red without a source change; the pins make a lint verdict depend on the repo alone - The job asserts each binary reports its pinned version, so a resolution that silently drifted cannot pass unnoticed Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- TEMPORARY trigger, reverted before merge: workflow_dispatch only reaches workflows already on the default branch, so a new one cannot be exercised - The runner becomes a matrix so a pull_request event covers both architectures Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- Seven rows for the flows that landed after the original Linux verification, each observed on x86_64 and aarch64 - The legend now names the workflow the rows come from, so a stale table is re-dispatchable rather than guesswork Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- The pull_request trigger existed to produce the matrix evidence while the workflow was not yet on the default branch; that run is recorded Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- The matrix in the Linux docs cites a run, so a new command, flag contract or manifest key that never reaches the smoke job leaves it asserting untested behaviour - Stated once under Testing, with a checkbox in the engine-changes checklist Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- One composite action holds the flows; the jobs that already build a release binary call it, so the coverage costs seconds instead of another build - macOS runs them against the universal binary users install, which nothing exercised end to end before - The manual workflow calls the same action, for reproducing a failure on a runner PR checks does not cover Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- The two are independent, and serialising them made Linux the critical path of PR checks; the smoke flows ride with the release build they need - Drop the unconditional apt-get update, which had grown to cost more than the toolchain download, and fall back to it only when a package will not resolve Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- A shellInteractive command gets a PTY only on a terminal; a picker that loses its input throws instead of answering - The Linux upgrade command runs only for a plain X.Y.Z tag, quotes its paths, and hands a sudo form to the user - Injected environments reach bootstrap's pack resolver and Homebrew prefixes; $HOME must be absolute, zsh is checked
- Lint uses pinned portable binaries on macOS, so SourceKit rules are enforced; release jobs hold write access only when publishing - Test and build run as per-platform matrices that keep the required check names; the manual Linux smoke workflow is gone - swiftly is pinned and checksummed, and smoke runs in a sandboxed HOME with a shellInteractive component under a time limit
- A missing Claude CLI prints the self-updating native installer on every platform; mcs never installs it itself - Allocation checks before fork collapse into one guard; the termios reader and Homebrew test seams leave production code - Tests mirror their sources and drop cases that only restated language behavior
- Comments state their reason once, without narration; helper comments move inside bodies so they stay plain comments - CLAUDE.md and the Linux doc list five platform files, describe the upgrade and Claude CLI behavior, and mark smoke-backed rows - Docs cover shellInteractive off a terminal and how to recover a tag whose release failed
bguidolim
force-pushed
the
bruno/ISSUE-262-linux-support
branch
from
October 9, 2026 17:08
bf34547 to
ba03f39
Compare
- Raw mode cleared ICANON and ECHO but not ISIG, so Ctrl-C killed the process before the restore ran and left the terminal with no echo and no cursor; the pickers' existing Ctrl-C branch was unreachable and returned the default, which would have read as consent once the byte arrived - A failing brew check without Homebrew is no longer planned for a re-sync, so doctor --fix shows the manual hint instead of repairing and failing again - A hook-interpreter test names `node` when it is missing; the Swift 6.4 link failure is recorded as a known limitation Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- AnyObject.isEqual resolves only where Foundation bridges to NSObject, so the per-sub-key merge did not compile on Glibc - Canonical JSON bytes settle it instead, which also stops `true` and `1` counting as the same value; either way an unequal result keeps the user's Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
- AnyObject.isEqual compiled on macOS and failed only on Linux, so the macOS jobs could not catch it; the rule now says where that class of call goes wrong and what replaces it for JSON values Claude-Session: https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mcs only ran on macOS, so a Linux user could not configure Claude Code with it at
all. It now builds and runs on glibc Linux with the same feature set, and the
release ships a tarball per architecture alongside the macOS universal binary.
Several bugs surfaced by the port also affected macOS: a raw-mode prompt spun at
100% CPU when stdin closed, a
shellInteractivecomponent spun (and could thenhang forever) when mcs ran without a terminal, and the process lock outlived mcs
if a component left a child running.
Replaces #395, which went stale against main.
Closes #262
Release note
shellInteractivecomponent no longer hangs or burns CPU when mcs runs without a terminal (CI, hooks,</dev/null); it runs without a PTY and its prompts see end of inputHOME=<dir> mcs …is honoured on macOS too; mcs previously wrote to the passwd home regardlessChanges
comes from swift-crypto, which re-exports CryptoKit on Apple platforms.
$HOMEis honoured on both platforms, including for~in packpaths and doctor checks; injected environments now reach every path resolver.
shellInteractivegets a PTY only on aterminal; the PTY bridge drains output before closing on an error.
X.Y.Ztag, quotes itspaths, and hands a
sudoform to the user rather than to Claude.rules), and test, build and smoke flows as per-platform matrices; the release
job alone holds write access, and swiftly is pinned and checksummed.
Test plan
swift testpasses locallyswiftformat --lint .andswiftlintpass without violations, on the pinned versionsmcs sync,mcs doctor).github/actions/mcs-smokepasses in PR checks onubuntu-22.04,ubuntu-22.04-armand the macOS universal binary, in a sandboxed$HOMEChecklist for engine changes
LifecycleIntegrationTestsorDoctorRunnerIntegrationTests)CLAUDE.md,docs/,techpack.yamlschema inExternalPackManifest.swift)https://claude.ai/code/session_014tZGxbvJUwXqd6fU1S3fXu