From 2f63e6610828935746f7162b59d422090837c33a Mon Sep 17 00:00:00 2001 From: Ben Phillips Date: Fri, 27 Jun 2025 12:24:27 +0100 Subject: [PATCH 1/2] Fix macOS extended attributes error in Docker container operations Resolves issue where macOS extended attributes (like com.apple.provenance) caused Docker putArchive operations to fail with "operation not supported" errors. Simplifies tar command construction to reliably exclude extended attributes on macOS using --no-xattrs and --no-fflags flags. Fixes #21 --- src/container.ts | 73 +++++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/src/container.ts b/src/container.ts index 4b2fa92..d8536d0 100644 --- a/src/container.ts +++ b/src/container.ts @@ -502,18 +502,6 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\ const { execSync } = require("child_process"); const fs = require("fs"); - // Helper function to get tar flags safely - const getTarFlags = () => { - try { - // Test if --no-xattrs is supported by checking tar help - execSync("tar --help 2>&1 | grep -q no-xattrs", { stdio: "pipe" }); - return "--no-xattrs"; - } catch { - // --no-xattrs not supported, use standard tar - return ""; - } - }; - try { // Get list of git-tracked files (including uncommitted changes) const trackedFiles = execSync("git ls-files", { @@ -590,19 +578,18 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\ // Also copy .git directory to preserve git history console.log(chalk.blue("• Copying git history...")); const gitTarFile = `/tmp/claude-sandbox-git-${Date.now()}.tar`; - // Exclude macOS resource fork files and .DS_Store when creating git archive - // Also strip extended attributes to prevent macOS xattr issues in Docker - const tarFlags = getTarFlags(); - // On macOS, also exclude extended attributes that cause Docker issues - const additionalFlags = (process.platform as string) === "darwin" ? "--no-xattrs --no-fflags" : ""; - const combinedFlags = `${tarFlags} ${additionalFlags}`.trim(); - execSync( - `tar -cf "${gitTarFile}" --exclude="._*" --exclude=".DS_Store" ${combinedFlags} .git`, - { - cwd: workDir, - stdio: "pipe", - }, - ); + + // On macOS, use --no-xattrs and --no-fflags to prevent extended attribute issues + let tarCommand = `tar -cf "${gitTarFile}" --exclude="._*" --exclude=".DS_Store"`; + if (process.platform === "darwin") { + tarCommand += " --no-xattrs --no-fflags"; + } + tarCommand += " .git"; + + execSync(tarCommand, { + cwd: workDir, + stdio: "pipe", + }); try { const gitStream = fs.createReadStream(gitTarFile); @@ -636,18 +623,6 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\ const path = require("path"); const { execSync } = require("child_process"); - // Helper function to get tar flags safely - const getTarFlags = () => { - try { - // Test if --no-xattrs is supported by checking tar help - execSync("tar --help 2>&1 | grep -q no-xattrs", { stdio: "pipe" }); - return "--no-xattrs"; - } catch { - // --no-xattrs not supported, use standard tar - return ""; - } - }; - try { // First, try to get credentials from macOS Keychain if on Mac if (process.platform === "darwin") { @@ -779,24 +754,26 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\ // Copy .claude directory if it exists (but skip if we already copied from Keychain) const claudeDir = path.join(os.homedir(), ".claude"); + const skipClaudeDir = process.platform === "darwin"; if ( fs.existsSync(claudeDir) && fs.statSync(claudeDir).isDirectory() && - process.platform !== "darwin" + !skipClaudeDir ) { console.log(chalk.blue("• Copying .claude directory...")); const tarFile = `/tmp/claude-dir-${Date.now()}.tar`; - const tarFlags = getTarFlags(); - // On macOS, also exclude extended attributes that cause Docker issues - const additionalFlags = (process.platform as string) === "darwin" ? "--no-xattrs --no-fflags" : ""; - const combinedFlags = `${tarFlags} ${additionalFlags}`.trim(); - execSync( - `tar -cf "${tarFile}" ${combinedFlags} -C "${os.homedir()}" .claude`, - { - stdio: "pipe", - }, - ); + + // On macOS, use --no-xattrs and --no-fflags to prevent extended attribute issues + let tarCommand = `tar -cf "${tarFile}"`; + if (process.platform === "darwin") { + tarCommand += " --no-xattrs --no-fflags"; + } + tarCommand += ` -C "${os.homedir()}" .claude`; + + execSync(tarCommand, { + stdio: "pipe", + }); const stream = fs.createReadStream(tarFile); await container.putArchive(stream, { From 339f344373f054a9d9d825c252664225fbfa99f9 Mon Sep 17 00:00:00 2001 From: Ben Phillips Date: Thu, 14 Aug 2025 11:36:18 +0100 Subject: [PATCH 2/2] Always rebuild image when custom Dockerfile is specified This ensures that any changes to a custom Dockerfile are immediately reflected in the built image, rather than using a potentially outdated cached version. --- src/container.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/container.ts b/src/container.ts index d8536d0..8e1ab78 100644 --- a/src/container.ts +++ b/src/container.ts @@ -66,22 +66,24 @@ export class ContainerManager { private async ensureImage(): Promise { const imageName = this.config.dockerImage || "claude-code-sandbox:latest"; - // Check if image already exists + // If a custom dockerfile is specified, always rebuild to ensure changes are applied + if (this.config.dockerfile) { + console.log(chalk.blue(`• Building image from custom Dockerfile: ${imageName}...`)); + await this.buildImage(this.config.dockerfile, imageName); + return; + } + + // For default builds, check if image already exists try { await this.docker.getImage(imageName).inspect(); console.log(chalk.green(`✓ Using existing image: ${imageName}`)); return; } catch (error) { - console.log(chalk.blue(`• Building image: ${imageName}...`)); + console.log(chalk.blue(`• Building default image: ${imageName}...`)); } - // Check if we need to build from Dockerfile - if (this.config.dockerfile) { - await this.buildImage(this.config.dockerfile, imageName); - } else { - // Use default Dockerfile - await this.buildDefaultImage(imageName); - } + // Use default Dockerfile + await this.buildDefaultImage(imageName); } private async buildDefaultImage(imageName: string): Promise {