Skip to content

Fix: copy workspace files with correct claude ownership #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
49 changes: 22 additions & 27 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export class ContainerManager {
}

async start(containerConfig: any): Promise<string> {
// Build or pull image
await this.ensureImage();

// Create container
Expand Down Expand Up @@ -497,6 +496,10 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\
const { execSync } = require("child_process");
const fs = require("fs");

// Set tar flags to create archives with correct ownership (UID/GID 1000:1000)
// This eliminates the need for post-copy chown operations in the container
const TAR_OWNER_FLAGS = "--numeric-owner --owner=1000 --group=1000";

try {
// Get list of git-tracked files (including uncommitted changes)
const trackedFiles = execSync("git ls-files", {
Expand Down Expand Up @@ -524,29 +527,18 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\

console.log(chalk.blue(`• Copying ${allFiles.length} files...`));

// Create tar archive using git archive for tracked files + untracked files
// Create a single tar archive with correct ownership from the start
// This replaces the previous git-archive + append approach and ensures
// all files have UID/GID 1000:1000 without needing container-side chown
const tarFile = `/tmp/claude-sandbox-${Date.now()}.tar`;
const fileListPath = `/tmp/claude-sandbox-files-${Date.now()}.txt`;
fs.writeFileSync(fileListPath, allFiles.join("\n"));

// First create archive of tracked files using git archive
execSync(`git archive --format=tar -o "${tarFile}" HEAD`, {
cwd: workDir,
stdio: "pipe",
});

// Add untracked files if any
if (untrackedFiles.length > 0) {
// Create a file list for tar
const fileListPath = `/tmp/claude-sandbox-files-${Date.now()}.txt`;
fs.writeFileSync(fileListPath, untrackedFiles.join("\n"));

// Append untracked files to the tar
execSync(`tar -rf "${tarFile}" --files-from="${fileListPath}"`, {
cwd: workDir,
stdio: "pipe",
});

fs.unlinkSync(fileListPath);
}
execSync(
`tar ${TAR_OWNER_FLAGS} -cf "${tarFile}" --files-from="${fileListPath}"`,
{ cwd: workDir, stdio: "pipe" },
);
fs.unlinkSync(fileListPath); // cleanup the temp list

// Read and copy the tar file in chunks to avoid memory issues
const stream = fs.createReadStream(tarFile);
Expand All @@ -573,11 +565,13 @@ 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 when creating git archive
execSync(`tar -cf "${gitTarFile}" --exclude="._*" .git`, {
cwd: workDir,
stdio: "pipe",
});

// Create .git archive with correct ownership and exclude macOS resource fork files
// Using the same ownership flags ensures git files are also owned by claude user
execSync(
`tar ${TAR_OWNER_FLAGS} -cf "${gitTarFile}" --exclude="._*" .git`,
{ cwd: workDir, stdio: "pipe" },
);

try {
const gitStream = fs.createReadStream(gitTarFile);
Expand All @@ -589,6 +583,7 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\

// Clean up
fs.unlinkSync(gitTarFile);

} catch (error) {
console.error(chalk.red("✗ Git history copy failed:"), error);
// Clean up the tar file even if upload failed
Expand Down