Merge pull request #379 from RayCharlizard/fix/issue-373-complete

fix: complete double-nested home path resolution (#373)
This commit is contained in:
Aaddrick
2026-04-03 22:19:15 -04:00
committed by GitHub

View File

@@ -176,6 +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));
}
/**
* Build a mount-name -> host-path mapping from mountBinds (prior
@@ -195,9 +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 = path.resolve(
path.join(homeDir, info.path)
);
const resolved = resolveSubpath(info.path);
if (resolved !== homeDir &&
!resolved.startsWith(homeDir + path.sep)) {
log(`buildMountMap: rejecting "${name}" — resolves outside home: ${resolved}`);
@@ -224,19 +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 &&
mergedEnv.CLAUDE_CONFIG_DIR.startsWith('/sessions/')) {
const translated = translateGuestPath(
mergedEnv.CLAUDE_CONFIG_DIR, mountMap
);
if (translated) {
log(`buildSpawnEnv: translated CLAUDE_CONFIG_DIR: ${mergedEnv.CLAUDE_CONFIG_DIR} -> ${translated}`);
mergedEnv.CLAUDE_CONFIG_DIR = translated;
} else {
log(`buildSpawnEnv: removing VM guest CLAUDE_CONFIG_DIR: ${mergedEnv.CLAUDE_CONFIG_DIR}`);
delete mergedEnv.CLAUDE_CONFIG_DIR;
}
}
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;
}
@@ -319,7 +347,7 @@ function resolvePluginRoot(pluginPath, mountBase) {
function resolveWorkDir(cwd, sharedCwdPath, mountMap) {
let workDir = cwd || os.homedir();
if (sharedCwdPath) {
workDir = path.join(os.homedir(), sharedCwdPath);
workDir = resolveSubpath(sharedCwdPath);
} else if (cwd && cwd.startsWith('/sessions/')) {
const translated = translateGuestPath(cwd, mountMap || {});
if (translated) {
@@ -346,7 +374,7 @@ function resolveWorkDir(cwd, sharedCwdPath, mountMap) {
function resolveSdkBinary(sdkSubpath, version, label) {
if (!sdkSubpath || !version) return null;
const candidatePath = path.join(
os.homedir(), sdkSubpath, version, 'claude'
resolveSubpath(sdkSubpath), version, 'claude'
);
try {
fs.accessSync(candidatePath, fs.constants.X_OK);
@@ -731,7 +759,7 @@ class HostBackend extends LocalBackend {
async mountPath(params) {
const { subpath } = params;
log(`HostBackend mountPath: ${subpath}`);
const guestPath = path.join('/', subpath || '');
const guestPath = resolveSubpath(subpath);
return { guestPath };
}
}
@@ -910,7 +938,7 @@ class BwrapBackend extends LocalBackend {
async mountPath(params) {
const { subpath, mountName } = params;
log(`BwrapBackend mountPath: ${mountName} -> ${subpath}`);
const hostPath = path.join('/', subpath || '');
const hostPath = resolveSubpath(subpath);
// Store for --bind on next spawn
this.mountBinds.set(mountName || subpath, hostPath);
return { guestPath: hostPath };
@@ -1720,7 +1748,7 @@ class KvmBackend extends BackendBase {
}
// No home share — return host path with a warning
const hostPath = path.join('/', subpath || '');
const hostPath = resolveSubpath(subpath);
log('KvmBackend: no home share, returning host path');
return { guestPath: hostPath };
}