Skip to content
Open
Changes from all commits
Commits
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
100 changes: 51 additions & 49 deletions scripts/cowork-vm-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,24 +176,24 @@ function translateGuestPath(guestPath, mountMap) {
log(`translateGuestPath: ${guestPath} -> ${normalized}`);
return normalized;
}

/**
* Resolve a subpath that may be root-relative (e.g. "home/user/.config/...")
* or home-relative (e.g. ".config/..."). app.asar generates root-relative
* subpaths via path.relative('/', absolutePath), so path.join('/', subpath)
* recovers the original absolute path. Falls back to home-relative for
* legacy or genuinely relative subpaths.
*
* Fix for https://github.com/aaddrick/claude-desktop-debian/issues/373
*/
function resolveSubpath(subpath) {
if (!subpath) return os.homedir();
const asRoot = path.resolve(path.join('/', subpath));
if (asRoot.startsWith(os.homedir() + path.sep) || asRoot === os.homedir()) {
return asRoot;
}
return path.resolve(path.join(os.homedir(), subpath));
}
/**
* Resolve a subpath that may be root-relative (e.g. "home/user/.config/...")
* or home-relative (e.g. ".config/..."). app.asar generates root-relative
* subpaths via path.relative('/', absolutePath), so path.join('/', subpath)
* recovers the original absolute path. Falls back to home-relative for
* legacy or genuinely relative subpaths.
*
* Fix for https://github.com/aaddrick/claude-desktop-debian/issues/373
*/
function resolveSubpath(subpath) {
if (!subpath) return os.homedir();
const asRoot = path.resolve(path.join('/', subpath));
if (asRoot.startsWith(os.homedir() + path.sep) || asRoot === os.homedir()) {
return asRoot;
}
return path.resolve(path.join(os.homedir(), subpath));
}

/**
* Build a mount-name -> host-path mapping from mountBinds (prior
Expand All @@ -213,7 +213,7 @@ function buildMountMap(additionalMounts, mountBinds) {
const homeDir = os.homedir();
for (const [name, info] of Object.entries(additionalMounts)) {
if (!info || !info.path) continue;
const resolved = resolveSubpath(info.path);
const resolved = resolveSubpath(info.path);
if (resolved !== homeDir &&
!resolved.startsWith(homeDir + path.sep)) {
log(`buildMountMap: rejecting "${name}" — resolves outside home: ${resolved}`);
Expand All @@ -240,31 +240,31 @@ function buildSpawnEnv(appEnv, mountMap) {

// Translate CLAUDE_CONFIG_DIR from guest path to host path, or
// remove it so Claude Code falls back to ~/.claude/.
if (mergedEnv.CLAUDE_CONFIG_DIR) {
if (mergedEnv.CLAUDE_CONFIG_DIR.startsWith('/sessions/')) {
// translate guest path to host path
const translated = translateGuestPath(
mergedEnv.CLAUDE_CONFIG_DIR, mountMap
);
if (translated !== mergedEnv.CLAUDE_CONFIG_DIR) {
log(`buildSpawnEnv: translated CLAUDE_CONFIG_DIR: ${mergedEnv.CLAUDE_CONFIG_DIR} -> ${translated}`);
mergedEnv.CLAUDE_CONFIG_DIR = translated;
}
} else {
// Host path — may be doubled by app.asar's own
// path.join(homedir, rootRelativeSubpath). Extract the
// relative part and resolve it properly.
const homeDir = os.homedir();
if (mergedEnv.CLAUDE_CONFIG_DIR.startsWith(homeDir + path.sep)) {
const relative = mergedEnv.CLAUDE_CONFIG_DIR.slice(homeDir.length + 1);
const fixed = resolveSubpath(relative);
if (fixed !== mergedEnv.CLAUDE_CONFIG_DIR) {
log(`buildSpawnEnv: fixed doubled CLAUDE_CONFIG_DIR: ${mergedEnv.CLAUDE_CONFIG_DIR} -> ${fixed}`);
mergedEnv.CLAUDE_CONFIG_DIR = fixed;
}
}
}
}
if (mergedEnv.CLAUDE_CONFIG_DIR) {
if (mergedEnv.CLAUDE_CONFIG_DIR.startsWith('/sessions/')) {
// translate guest path to host path
const translated = translateGuestPath(
mergedEnv.CLAUDE_CONFIG_DIR, mountMap
);
if (translated !== mergedEnv.CLAUDE_CONFIG_DIR) {
log(`buildSpawnEnv: translated CLAUDE_CONFIG_DIR: ${mergedEnv.CLAUDE_CONFIG_DIR} -> ${translated}`);
mergedEnv.CLAUDE_CONFIG_DIR = translated;
}
} else {
// Host path — may be doubled by app.asar's own
// path.join(homedir, rootRelativeSubpath). Extract the
// relative part and resolve it properly.
const homeDir = os.homedir();
if (mergedEnv.CLAUDE_CONFIG_DIR.startsWith(homeDir + path.sep)) {
const relative = mergedEnv.CLAUDE_CONFIG_DIR.slice(homeDir.length + 1);
const fixed = resolveSubpath(relative);
if (fixed !== mergedEnv.CLAUDE_CONFIG_DIR) {
log(`buildSpawnEnv: fixed doubled CLAUDE_CONFIG_DIR: ${mergedEnv.CLAUDE_CONFIG_DIR} -> ${fixed}`);
mergedEnv.CLAUDE_CONFIG_DIR = fixed;
}
}
}
}

return mergedEnv;
}
Expand Down Expand Up @@ -857,15 +857,17 @@ class BwrapBackend extends LocalBackend {
log('BwrapBackend: could not resolve /etc/resolv.conf:', e.message);
}

// Bind the SDK binary read-only
const sdkDir = path.dirname(actualCommand);
bwrapArgs.push('--ro-bind', sdkDir, sdkDir);

// Create home directory (needed for ~ expansion) but don't
// expose real home contents.
// expose real home contents. Must come before any --ro-bind of
// subdirectories inside $HOME (e.g. the SDK binary path), otherwise
// bwrap processes --dir after the bind and shadows it.
const homeDir = os.homedir();
bwrapArgs.push('--dir', homeDir);

// Bind the SDK binary read-only
const sdkDir = path.dirname(actualCommand);
bwrapArgs.push('--ro-bind', sdkDir, sdkDir);

// Create /sessions/<name>/mnt/ guest path structure and mount
// host directories at guest paths, matching the KVM backend
// layout. The claude-code-vm binary translates all paths to
Expand Down