Skip to content

Commit 246233e

Browse files
Mossakaclaude
andcommitted
fix: mount empty writable HOME directory for chroot mode
When only subdirectories of $HOME are bind-mounted (e.g., ~/.cargo, ~/.claude), Docker creates $HOME as an empty root-owned directory. Tools that need to write directly to $HOME (creating lock files, temp files, or new subdirectories) hang or fail. Mount a clean writable directory as $HOME in the chroot, with the specific subdirectory mounts layered on top. This gives tools a writable $HOME without exposing any credential files from the host. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1aee684 commit 246233e

1 file changed

Lines changed: 24 additions & 17 deletions

File tree

src/docker-manager.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,16 @@ export function generateDockerCompose(
484484
// Mount workspace directory at /host path for chroot
485485
agentVolumes.push(`${workspaceDir}:/host${workspaceDir}:rw`);
486486

487+
// Mount an empty writable home directory at /host$HOME
488+
// This gives tools a writable $HOME without exposing credential files.
489+
// The specific subdirectory mounts below (.cargo, .claude, etc.) overlay
490+
// on top, providing access to only the directories we explicitly mount.
491+
// Without this, $HOME inside the chroot is an empty root-owned directory
492+
// created by Docker as a side effect of subdirectory mounts, which causes
493+
// tools like rustc and Claude Code to hang or fail.
494+
const emptyHomeDir = path.join(config.workDir, 'chroot-home');
495+
agentVolumes.push(`${emptyHomeDir}:/host${effectiveHome}:rw`);
496+
487497
// /tmp is needed for chroot mode to write:
488498
// - Temporary command scripts: /host/tmp/awf-cmd-$$.sh
489499
// - One-shot token LD_PRELOAD library: /host/tmp/awf-lib/one-shot-token.so
@@ -968,29 +978,26 @@ export async function writeConfigs(config: WrapperConfig): Promise<void> {
968978
logger.debug(`MCP logs directory permissions fixed at: ${mcpLogsDir}`);
969979
}
970980

971-
// Ensure chroot home directory and subdirectories exist with correct ownership
972-
// before Docker bind-mounts them. If a source directory doesn't exist, Docker
973-
// creates it as root:root, making it inaccessible to the agent user (e.g., UID 1001).
974-
// This is critical for CLI tools that need writable home subdirectories
975-
// (e.g., Claude Code needs ~/.claude, npm needs ~/.npm).
976-
// We also ensure $HOME itself has correct ownership because Docker's creation of
977-
// subdirectory mounts creates parent directories as root:root, which prevents
978-
// tools from creating new files/directories directly in $HOME.
981+
// Ensure chroot home subdirectories exist with correct ownership before Docker
982+
// bind-mounts them. If a source directory doesn't exist, Docker creates it as
983+
// root:root, making it inaccessible to the agent user (e.g., UID 1001).
984+
// Also create an empty writable home directory that gets mounted as $HOME
985+
// in the chroot, giving tools a writable home without exposing credentials.
979986
if (config.enableChroot) {
980987
const effectiveHome = getRealUserHome();
981988
const uid = parseInt(getSafeHostUid(), 10);
982989
const gid = parseInt(getSafeHostGid(), 10);
983990

984-
// Ensure $HOME exists and is owned by the agent user
985-
// (Docker creates parent directories as root when bind-mounting subdirectories)
986-
if (fs.existsSync(effectiveHome)) {
987-
const stats = fs.statSync(effectiveHome);
988-
if (stats.uid !== uid || stats.gid !== gid) {
989-
fs.chownSync(effectiveHome, uid, gid);
990-
logger.debug(`Fixed ownership of ${effectiveHome} to ${uid}:${gid}`);
991-
}
991+
// Create empty writable home directory for the chroot
992+
// This is mounted as $HOME inside the container so tools can write to it
993+
const emptyHomeDir = path.join(config.workDir, 'chroot-home');
994+
if (!fs.existsSync(emptyHomeDir)) {
995+
fs.mkdirSync(emptyHomeDir, { recursive: true });
992996
}
997+
fs.chownSync(emptyHomeDir, uid, gid);
998+
logger.debug(`Created chroot home directory: ${emptyHomeDir} (${uid}:${gid})`);
993999

1000+
// Ensure source directories for subdirectory mounts exist with correct ownership
9941001
const chrootHomeDirs = [
9951002
'.copilot', '.cache', '.config', '.local',
9961003
'.anthropic', '.claude', '.cargo', '.rustup', '.npm',
@@ -1000,7 +1007,7 @@ export async function writeConfigs(config: WrapperConfig): Promise<void> {
10001007
if (!fs.existsSync(dirPath)) {
10011008
fs.mkdirSync(dirPath, { recursive: true });
10021009
fs.chownSync(dirPath, uid, gid);
1003-
logger.debug(`Created chroot home directory: ${dirPath} (${uid}:${gid})`);
1010+
logger.debug(`Created host home subdirectory: ${dirPath} (${uid}:${gid})`);
10041011
}
10051012
}
10061013
}

0 commit comments

Comments
 (0)