diff --git a/README.md b/README.md index 1c76f51..8687d20 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ Options: --no-web Disable web UI (use terminal attach) --no-push Disable automatic branch pushing --no-pr Disable automatic PR creation + --mount-folder Enable mounted folder mode + --mount-path Specify custom mount path ``` #### `claude-sandbox attach [container-id]` @@ -219,6 +221,8 @@ Create a `claude-sandbox.config.json` file (see `claude-sandbox.config.example.j - `containerPrefix`: Custom prefix for container names - `claudeConfigPath`: Path to Claude configuration file - `dockerSocketPath`: Custom Docker/Podman socket path (auto-detected by default) +- `useMountedFolder`: Enable mounted folder mode (default: false) +- `mountedFolderPath`: Path to mount into the container (default: current working directory) #### Mount Configuration @@ -237,6 +241,38 @@ Example use cases: ## Features +### Mounted Folder Mode + +Claude Code Sandbox supports mounting directories directly into containers instead of copying files. This is useful for: + +- **Large datasets**: Avoid copying overhead for large files +- **Non-git directories**: Work with folders that aren't part of a git repository +- **Real-time changes**: File changes sync instantly without copying delays +- **Performance**: Direct filesystem access is faster than copying + +#### Using Mounted Folder Mode + +Via CLI: + +```bash +# Mount current directory +claude-sandbox start --mount-folder + +# Mount a specific directory +claude-sandbox start --mount-folder --mount-path /path/to/directory +``` + +Via configuration file: + +```json +{ + "useMountedFolder": true, + "mountedFolderPath": "/path/to/directory" +} +``` + +**Note**: In mounted folder mode, if the directory isn't a git repository, git operations will be skipped and git monitoring will be disabled. This allows Claude Code to work with any directory, not just git repositories. + ### Podman Support Claude Code Sandbox now supports Podman as an alternative to Docker. The tool automatically detects whether you're using Docker or Podman by checking for available socket paths: diff --git a/src/cli.ts b/src/cli.ts index 07c1996..c021eab 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -82,6 +82,8 @@ program "Start with 'claude' or 'bash' shell", /^(claude|bash)$/i, ) + .option("--mount-folder", "Enable mounted folder mode") + .option("--mount-path ", "Specify custom mount path") .action(async (options) => { console.log(chalk.blue("🚀 Starting Claude Sandbox...")); @@ -90,6 +92,12 @@ program if (options.shell) { config.defaultShell = options.shell.toLowerCase(); } + if (options.mountFolder) { + config.useMountedFolder = true; + } + if (options.mountPath) { + config.mountedFolderPath = options.mountPath; + } const sandbox = new ClaudeSandbox(config); await sandbox.run(); @@ -125,6 +133,8 @@ program "Start with 'claude' or 'bash' shell", /^(claude|bash)$/i, ) + .option("--mount-folder", "Enable mounted folder mode") + .option("--mount-path ", "Specify custom mount path") .action(async (options) => { console.log(chalk.blue("🚀 Starting new Claude Sandbox container...")); @@ -139,6 +149,12 @@ program if (options.shell) { config.defaultShell = options.shell.toLowerCase(); } + if (options.mountFolder) { + config.useMountedFolder = true; + } + if (options.mountPath) { + config.mountedFolderPath = options.mountPath; + } const sandbox = new ClaudeSandbox(config); await sandbox.run(); diff --git a/src/container.ts b/src/container.ts index 4b2fa92..bfc1bee 100644 --- a/src/container.ts +++ b/src/container.ts @@ -25,19 +25,32 @@ export class ContainerManager { await container.start(); console.log(chalk.green("✓ Container started")); - // Copy working directory into container - console.log(chalk.blue("• Copying files into container...")); - try { - await this._copyWorkingDirectory(container, containerConfig.workDir); - console.log(chalk.green("✓ Files copied")); + // Copy working directory into container (unless using mounted folder mode) + if (!this.config.useMountedFolder) { + console.log(chalk.blue("• Copying files into container...")); + try { + await this._copyWorkingDirectory(container, containerConfig.workDir); + console.log(chalk.green("✓ Files copied")); + } catch (error) { + console.error(chalk.red("✗ File copy failed:"), error); + // Clean up container on failure + await container.stop().catch(() => {}); + await container.remove().catch(() => {}); + this.containers.delete(container.id); + throw error; + } + } else { + console.log(chalk.blue("• Using mounted folder mode (skipping file copy)")); + } - // Copy Claude configuration if it exists + // Copy Claude configuration if it exists + try { await this._copyClaudeConfig(container); // Copy git configuration if it exists await this._copyGitConfig(container); } catch (error) { - console.error(chalk.red("✗ File copy failed:"), error); + console.error(chalk.red("✗ Configuration copy failed:"), error); // Clean up container on failure await container.stop().catch(() => {}); await container.remove().catch(() => {}); @@ -414,9 +427,17 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\ _workDir: string, _credentials: Credentials, ): string[] { - // NO MOUNTING workspace - we'll copy files instead const volumes: string[] = []; + // Mount workspace directly if in mounted folder mode + if (this.config.useMountedFolder) { + const mountPath = this.config.mountedFolderPath || process.cwd(); + console.log( + chalk.blue(`✓ Mounting folder: ${mountPath} → /workspace`), + ); + volumes.push(`${mountPath}:/workspace`); + } + // NO SSH mounting - we'll use GitHub tokens instead // Add custom volumes (legacy format) diff --git a/src/index.ts b/src/index.ts index 9f860a5..1fe9f9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,12 +39,28 @@ export class ClaudeSandbox { async run(): Promise { try { - // Verify we're in a git repository - await this.verifyGitRepo(); + // Verify we're in a git repository (unless using mounted folder mode) + let isGitRepo = true; + if (!this.config.useMountedFolder) { + await this.verifyGitRepo(); + } else { + // Check if it's a git repo, but don't fail if it's not + isGitRepo = await this.git.checkIsRepo(); + if (!isGitRepo) { + console.log( + chalk.yellow( + "⚠ Not a git repository (OK for mounted folder mode)", + ), + ); + } + } - // Check current branch - const currentBranch = await this.git.branchLocal(); - console.log(chalk.blue(`Current branch: ${currentBranch.current}`)); + // Check current branch (only if it's a git repo) + let currentBranch = null; + if (isGitRepo) { + currentBranch = await this.git.branchLocal(); + console.log(chalk.blue(`Current branch: ${currentBranch.current}`)); + } // Determine target branch based on config options (but don't checkout in host repo) let branchName = ""; @@ -148,13 +164,17 @@ export class ClaudeSandbox { chalk.green(`✓ Started container: ${containerId.substring(0, 12)}`), ); - // Start monitoring for commits - this.gitMonitor.on("commit", async (commit) => { - await this.handleCommit(commit); - }); + // Start monitoring for commits (only if it's a git repo) + if (isGitRepo) { + this.gitMonitor.on("commit", async (commit) => { + await this.handleCommit(commit); + }); - await this.gitMonitor.start(branchName); - console.log(chalk.blue("✓ Git monitoring started")); + await this.gitMonitor.start(branchName); + console.log(chalk.blue("✓ Git monitoring started")); + } else { + console.log(chalk.yellow("⚠ Skipping git monitoring (not a git repo)")); + } // Always launch web UI this.webServer = new WebUIServer(this.docker); diff --git a/src/types.ts b/src/types.ts index 5c641be..d986d9a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,8 @@ export interface SandboxConfig { remoteBranch?: string; prNumber?: string; dockerSocketPath?: string; + useMountedFolder?: boolean; + mountedFolderPath?: string; } export interface Credentials {