Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
1c40a42
Prove NODE_OPTIONS restore module needs durable storage
austinywang May 5, 2026
ce718c7
Keep Claude NODE_OPTIONS preload out of temp cleanup
austinywang May 5, 2026
aa37279
Merge remote-tracking branch 'origin/main' into issue-3512-node-optio…
austinywang May 5, 2026
67aaf4a
Share NODE_OPTIONS parsing across Swift targets
austinywang May 5, 2026
4d14ef9
Keep NODE_OPTIONS regressions inside Swift file budgets
austinywang May 5, 2026
c0099d9
Prove NODE_OPTIONS tokenizers must respect quoted paths
austinywang May 5, 2026
c3af32b
Tokenize NODE_OPTIONS consistently outside Swift
austinywang May 5, 2026
0050f72
Close NODE_OPTIONS review gaps across launch surfaces
austinywang May 5, 2026
09f3a01
Fail fast in NODE_OPTIONS regression helpers
austinywang May 5, 2026
0b5cddb
Preserve independent build graph additions during main merge
austinywang May 5, 2026
8e0437c
Keep restore overrides sanitizer-visible
austinywang May 5, 2026
3911a9e
Keep wrapper arrays safe on macOS bash
austinywang May 5, 2026
68f76d3
Preserve launch refactor while hardening node restore sanitization
austinywang May 5, 2026
fd529be
Run restore override wrapper regression
austinywang May 5, 2026
15cdd21
Prove NODE_OPTIONS keeps apostrophes
austinywang May 5, 2026
48f1e3b
Treat apostrophes as NODE_OPTIONS literals
austinywang May 5, 2026
6834041
Sync PR branch with current release base
austinywang May 5, 2026
ed48c70
Preserve literal backslashes in NODE_OPTIONS
austinywang May 5, 2026
3075dd6
Sync NODE_OPTIONS PR with v0.64.1 base
austinywang May 5, 2026
9122d3b
Prevent stale cmux NODE_OPTIONS preloads from surviving reinjection
austinywang May 6, 2026
5f24f5f
Keep CLI NODE_OPTIONS reinjection from retaining stale preloads
austinywang May 6, 2026
224302e
Share NODE_OPTIONS token helpers across Swift launch paths
austinywang May 6, 2026
0290df4
Normalize restored Go NODE_OPTIONS heap flags
austinywang May 6, 2026
2e13071
Require exact cmux path components in bash preload cleanup
austinywang May 6, 2026
9b9e994
Preserve user NODE_OPTIONS heap caps during reinjection
austinywang May 6, 2026
a3f6fd0
Retry CI after transient Zig download failure
austinywang May 6, 2026
84dfcab
Cover inline user heap caps during wrapper reinjection
austinywang May 6, 2026
501b58b
Retry transient Zig downloads in CircleCI
austinywang May 6, 2026
3546372
Resume Zig downloads across CircleCI retries
austinywang May 6, 2026
72d300e
Prefer Homebrew Zig on CircleCI macOS
austinywang May 6, 2026
0793b6a
Use bottled Zig before CircleCI tarball fallback
austinywang May 6, 2026
f25e107
Avoid corrupt Zig tarball retries
austinywang May 6, 2026
396a187
Merge main to keep PR current
austinywang May 6, 2026
24292b8
Merge latest main for Hermes agent updates
austinywang May 7, 2026
67fde3f
Keep CLI NODE_OPTIONS logic under file budget
austinywang May 7, 2026
5560937
Keep NODE_OPTIONS PR current with main
austinywang May 11, 2026
340eec3
Link NODE_OPTIONS package into CLI test builds
austinywang May 11, 2026
76583ef
Merge origin/main into issue-3512-node-options-tmpdir
austinywang May 12, 2026
fc3059d
Merge origin/main into issue-3512-node-options-tmpdir
austinywang May 12, 2026
2e45f21
Share cmux NODE_OPTIONS stripping in Swift
austinywang May 12, 2026
aba1956
Share NODE_OPTIONS sanitizer policy
austinywang May 12, 2026
d3ef942
Treat stale cmux NODE_OPTIONS as absent
austinywang May 13, 2026
de13a27
Merge origin/main into issue-3512-node-options-tmpdir
austinywang May 13, 2026
c9dae9d
Fix NODE_OPTIONS review feedback
austinywang May 14, 2026
bce868d
Fix NODE_OPTIONS fallback temp reuse
austinywang May 17, 2026
8673ad4
Merge remote-tracking branch 'origin/main' into issue-3512-node-optio…
austinywang May 18, 2026
a0d68d3
Fix remote daemon test import
austinywang May 18, 2026
bdaf095
Merge origin/main and harden NODE_OPTIONS restore dirs
austinywang May 18, 2026
1edefd8
Merge remote-tracking branch 'origin/main' into issue-3512-node-optio…
austinywang May 18, 2026
8567191
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang May 19, 2026
b9df3ac
Fix NODE_OPTIONS restore directory validation
austinywang May 19, 2026
4a3aac3
Align NODE_OPTIONS restore normalization guard
austinywang May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CLI/CMUXCLI+NodeOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation
import CMUXNodeOptions

extension CMUXCLI {
func mergedNodeOptions(existing: String?, restoreModulePath: String) -> String {
let requireOption = "--require=\(NodeOptionsSupport.requirePath(restoreModulePath))"
let memoryOption = "--max-old-space-size=4096"
guard let cleanedExisting = NodeOptionsSupport.sanitizedNodeOptions(existing) else {
return "\(requireOption) \(memoryOption)"
}
return "\(requireOption) \(memoryOption) \(cleanedExisting)"
}

func normalizedNodeOptionsForRestore(_ existing: String) -> String? {
NodeOptionsSupport.normalizedNodeOptionsForRestore(existing)
}

}
81 changes: 19 additions & 62 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import CMUXAgentLaunch
import CoreFoundation
import CryptoKit
import Darwin
import CMUXNodeOptions
#if canImport(LocalAuthentication)
import LocalAuthentication
#endif
Expand Down Expand Up @@ -13189,9 +13190,10 @@ struct CMUXCLI {
unsetenv("CMUX_ORIGINAL_NODE_OPTIONS")
return
}
if let existing = processEnvironment["NODE_OPTIONS"] {
if let existing = processEnvironment["NODE_OPTIONS"],
let originalNodeOptions = normalizedNodeOptionsForRestore(existing) {
setenv("CMUX_ORIGINAL_NODE_OPTIONS_PRESENT", "1", 1)
setenv("CMUX_ORIGINAL_NODE_OPTIONS", normalizedNodeOptionsForRestore(existing), 1)
setenv("CMUX_ORIGINAL_NODE_OPTIONS", originalNodeOptions, 1)
} else {
setenv("CMUX_ORIGINAL_NODE_OPTIONS_PRESENT", "0", 1)
unsetenv("CMUX_ORIGINAL_NODE_OPTIONS")
Expand Down Expand Up @@ -13233,14 +13235,23 @@ struct CMUXCLI {
}

private func createClaudeNodeOptionsRestoreModule() throws -> URL {
let root = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent("cmux-claude-node-options", isDirectory: true)
let root = try claudeNodeOptionsRestoreDirectory()
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true, attributes: nil)
let restoreModuleURL = root.appendingPathComponent("restore-node-options.cjs", isDirectory: false)
let restoreModuleURL = root.appendingPathComponent(NodeOptionsSupport.restoreModuleFilename, isDirectory: false)
try writeShimIfChanged(Self.claudeNodeOptionsRestoreModule, to: restoreModuleURL)
return restoreModuleURL
}

private func claudeNodeOptionsRestoreDirectory() throws -> URL {
try NodeOptionsSupport.claudeRestoreDirectory(
homePath: ProcessInfo.processInfo.environment["HOME"],
appSupportDirectory: FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first
)
}

private func runClaudeTeams(
Comment thread
coderabbitai[bot] marked this conversation as resolved.
commandArgs: [String],
socketPath: String,
Expand Down Expand Up @@ -15182,9 +15193,10 @@ struct CMUXCLI {
unsetenv("CMUX_ORIGINAL_NODE_OPTIONS")
return
}
if let existing = processEnvironment["NODE_OPTIONS"] {
if let existing = processEnvironment["NODE_OPTIONS"],
let originalNodeOptions = normalizedNodeOptionsForRestore(existing) {
setenv("CMUX_ORIGINAL_NODE_OPTIONS_PRESENT", "1", 1)
setenv("CMUX_ORIGINAL_NODE_OPTIONS", normalizedNodeOptionsForRestore(existing), 1)
setenv("CMUX_ORIGINAL_NODE_OPTIONS", originalNodeOptions, 1)
} else {
setenv("CMUX_ORIGINAL_NODE_OPTIONS_PRESENT", "0", 1)
unsetenv("CMUX_ORIGINAL_NODE_OPTIONS")
Expand Down Expand Up @@ -18771,61 +18783,6 @@ struct CMUXCLI {
}
}

private func mergedNodeOptions(existing: String?, restoreModulePath: String) -> String {
let requireOption = "--require=\(restoreModulePath)"
let memoryOption = "--max-old-space-size=4096"
let cleanedExisting = cleanedNodeOptions(existing)
guard !cleanedExisting.isEmpty else {
return "\(requireOption) \(memoryOption)"
}
return "\(requireOption) \(memoryOption) \(cleanedExisting)"
}

private func cleanedNodeOptions(_ existing: String?) -> String {
let tokens = (existing ?? "")
.split(whereSeparator: \.isWhitespace)
.map(String.init)
guard !tokens.isEmpty else { return "" }

var filtered: [String] = []
var index = 0
while index < tokens.count {
let token = tokens[index]
if token == "--max-old-space-size" {
index += min(2, tokens.count - index)
continue
}
if token.hasPrefix("--max-old-space-size=") {
index += 1
continue
}
filtered.append(token)
index += 1
}
return filtered.joined(separator: " ")
}

private func normalizedNodeOptionsForRestore(_ existing: String) -> String {
let tokens = existing
.split(whereSeparator: \.isWhitespace)
.map(String.init)
guard !tokens.isEmpty else { return "" }

var normalized: [String] = []
var index = 0
while index < tokens.count {
let token = tokens[index]
if token == "--max-old-space-size", index + 1 < tokens.count {
normalized.append("--max-old-space-size=\(tokens[index + 1])")
index += 2
continue
}
normalized.append(token)
index += 1
}
return normalized.joined(separator: " ")
}

// MARK: - Codex hooks

/// The hooks.json content that cmux installs into ~/.codex/.
Expand Down
3 changes: 2 additions & 1 deletion Packages/CMUXAgentLaunch/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ let package = Package(
],
dependencies: [
.package(path: "../CMUXAgentVault"),
.package(path: "../CMUXNodeOptions"),
],
targets: [
.target(
name: "CMUXAgentLaunch",
dependencies: ["CMUXAgentVault"]
dependencies: ["CMUXAgentVault", "CMUXNodeOptions"]
),
.testTarget(
name: "CMUXAgentLaunchTests",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import CMUXNodeOptions

public enum ClaudeConfigDirectoryPath {
public static func preferredPath(
Expand Down Expand Up @@ -87,7 +88,7 @@ public enum AgentLaunchEnvironmentPolicy {
case "CLAUDE_CONFIG_DIR":
return value.map { ClaudeConfigDirectoryPath.preferredPath($0) }
case "NODE_OPTIONS":
return sanitizedNodeOptions(value)
return NodeOptionsSupport.normalizedNodeOptionsForRestore(value)
default:
return value
}
Expand All @@ -96,93 +97,19 @@ public enum AgentLaunchEnvironmentPolicy {
private static func selectedNodeOptions(from env: [String: String]) -> String? {
switch normalizedValue(env["CMUX_ORIGINAL_NODE_OPTIONS_PRESENT"]) {
case "1":
return sanitizedNodeOptions(env["CMUX_ORIGINAL_NODE_OPTIONS"])
return NodeOptionsSupport.normalizedNodeOptionsForRestore(env["CMUX_ORIGINAL_NODE_OPTIONS"])
case "0":
return nil
default:
return sanitizedNodeOptions(env["NODE_OPTIONS"])
return NodeOptionsSupport.normalizedNodeOptionsForRestore(env["NODE_OPTIONS"])
}
}

private static func sanitizedNodeOptions(_ rawValue: String?) -> String? {
let tokens = rawValue?
.split(whereSeparator: \.isWhitespace)
.map(String.init) ?? []
guard !tokens.isEmpty else { return nil }

var sanitized: [String] = []
var index = 0
var shouldDropInjectedHeapCap = false
while index < tokens.count {
let token = tokens[index]

if shouldDropInjectedHeapCap, isInjectedNodeHeapCap(tokens, index: index) {
index += nodeHeapCapWidth(tokens, index: index)
shouldDropInjectedHeapCap = false
continue
}
shouldDropInjectedHeapCap = false

if isRequireOption(token), index + 1 < tokens.count,
isCmuxNodeOptionsRestoreModulePath(tokens[index + 1]) {
index += 2
shouldDropInjectedHeapCap = true
continue
}
if let path = inlineRequireOptionPath(token),
isCmuxNodeOptionsRestoreModulePath(path) {
index += 1
shouldDropInjectedHeapCap = true
continue
}

sanitized.append(token)
index += 1
}

let joined = sanitized.joined(separator: " ")
.trimmingCharacters(in: .whitespacesAndNewlines)
return joined.isEmpty ? nil : joined
}

private static func normalizedValue(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty else {
return nil
}
return trimmed
}

private static func isRequireOption(_ token: String) -> Bool {
token == "--require" || token == "-r"
}

private static func inlineRequireOptionPath(_ token: String) -> String? {
for prefix in ["--require=", "-r="] where token.hasPrefix(prefix) {
return String(token.dropFirst(prefix.count))
}
return nil
}

private static func isCmuxNodeOptionsRestoreModulePath(_ value: String) -> Bool {
let trimmed = value.trimmingCharacters(in: CharacterSet(charactersIn: "'\""))
guard URL(fileURLWithPath: trimmed).lastPathComponent == "restore-node-options.cjs" else {
return false
}
return trimmed.contains("/cmux-")
}

private static func isInjectedNodeHeapCap(_ tokens: [String], index: Int) -> Bool {
guard index < tokens.count else { return false }
let token = tokens[index]
if token == "--max-old-space-size" {
return index + 1 < tokens.count && tokens[index + 1] == "4096"
}
return token == "--max-old-space-size=4096"
}

private static func nodeHeapCapWidth(_ tokens: [String], index: Int) -> Int {
guard index < tokens.count else { return 1 }
return tokens[index] == "--max-old-space-size" ? min(2, tokens.count - index) : 1
}
}
22 changes: 22 additions & 0 deletions Packages/CMUXNodeOptions/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// swift-tools-version: 5.9

import PackageDescription

let package = Package(
name: "CMUXNodeOptions",
platforms: [
.macOS(.v13),
],
products: [
.library(
name: "CMUXNodeOptions",
targets: ["CMUXNodeOptions"]
),
],
targets: [
.target(
name: "CMUXNodeOptions",
path: "Sources/CMUXNodeOptions"
),
]
)
Loading
Loading