Add mounted folder mode support

Implement mounted folder mode to allow direct directory mounting instead of
copying files into containers. This enables better performance for large
datasets, support for non-git directories, and real-time file changes.

Features:
- Added --mount-folder and --mount-path CLI options
- Added useMountedFolder and mountedFolderPath config properties
- Made file copying conditional based on mode
- Skip git operations gracefully for non-git directories
- Added comprehensive documentation

Authored-By: Gregory Bensa <https://github.com/GregoryBensa>
Original commit: 13fc6ac543
This commit is contained in:
firepol 2025-11-02 12:23:17 +00:00
parent 61cfa97de5
commit ddffc03b1f
5 changed files with 114 additions and 19 deletions

View file

@ -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 <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:

View file

@ -82,6 +82,8 @@ program
"Start with 'claude' or 'bash' shell",
/^(claude|bash)$/i,
)
.option("--mount-folder", "Enable mounted folder mode")
.option("--mount-path <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 <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();

View file

@ -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)

View file

@ -39,12 +39,28 @@ export class ClaudeSandbox {
async run(): Promise<void> {
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);

View file

@ -26,6 +26,8 @@ export interface SandboxConfig {
remoteBranch?: string;
prNumber?: string;
dockerSocketPath?: string;
useMountedFolder?: boolean;
mountedFolderPath?: string;
}
export interface Credentials {