mirror of
https://github.com/textcortex/claude-code-sandbox.git
synced 2025-07-07 13:25:10 +00:00
8.8 KiB
8.8 KiB
Safe Git Operations Implementation Plan
Overview
This plan outlines how to safely handle Git operations (commit, push, PR) outside the container while maintaining security and seamless UX.
Architecture
1. Dual Repository Approach
- Container Repo: Claude works in an isolated git repo inside the container
- Shadow Repo: A temporary bare repository outside the container that mirrors commits
- Host Repo: The original repository remains untouched
2. Key Components
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Container Repo │────▶│ Shadow Repo │────▶│ Remote GitHub │
│ (Claude works) │ │ (Outside container)│ │ (Push/PR) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ │
└───── Git Bundle ──────┘
Implementation Details
Phase 1: Commit Extraction System
-
Git Bundle Mechanism
# Inside container git bundle create /tmp/changes.bundle <branch> ^origin/<branch> # Outside container docker cp <container>:/tmp/changes.bundle ./
-
Shadow Repository Setup (Optimized)
class ShadowRepository { private shadowPath: string; async initialize(originalRepo: string, branch: string) { // Create minimal single-branch clone this.shadowPath = await mkdtemp("/tmp/claude-shadow-"); // Option 1: Shallow single-branch clone (most efficient) await exec( `git clone --single-branch --branch ${branch} --depth 1 --bare ${originalRepo} ${this.shadowPath}`, ); // Option 2: Even more minimal - just init and add remote // await exec(`git init --bare ${this.shadowPath}`); // await exec(`git remote add origin ${originalRepo}`, { cwd: this.shadowPath }); // await exec(`git fetch origin ${branch}:${branch} --depth 1`, { cwd: this.shadowPath }); } async applyBundle(bundlePath: string, branch: string) { // Fetch commits from bundle await exec(`git fetch ${bundlePath} ${branch}:${branch}`, { cwd: this.shadowPath, }); } async cleanup() { // Remove shadow repo after use await fs.rm(this.shadowPath, { recursive: true }); } }
Phase 2: Git Operations Handler
interface GitOperation {
type: "commit" | "push" | "pr";
branch: string;
commits: CommitInfo[];
}
class GitOperationsHandler {
private shadow: ShadowRepository;
private githubToken?: string;
async extractCommits(containerId: string, branch: string) {
// 1. Create bundle in container
await docker.exec(containerId, [
"git",
"bundle",
"create",
"/tmp/changes.bundle",
branch,
`^origin/${branch}`,
]);
// 2. Copy bundle out
const bundlePath = await docker.copyFromContainer(
containerId,
"/tmp/changes.bundle",
);
// 3. Apply to shadow repo
await this.shadow.applyBundle(bundlePath, branch);
// 4. Get commit info
return await this.shadow.getCommits(branch);
}
async push(branch: string) {
// Use GitHub token from host environment
const token = await this.getGitHubToken();
await this.shadow.push(branch, token);
}
private async getGitHubToken() {
// Try multiple sources in order:
// 1. Environment variable
// 2. GitHub CLI (gh auth token)
// 3. Git credential helper
// 4. macOS Keychain
}
}
Phase 3: UI Flow
interface SessionEndOptions {
hasChanges: boolean;
branch: string;
commits: CommitInfo[];
}
class SessionEndUI {
async showOptions(options: SessionEndOptions) {
if (!options.hasChanges) {
return; // No UI needed
}
const choices = [
{
name: `Review ${options.commits.length} commits`,
value: "review",
},
{
name: "Push to branch",
value: "push",
disabled: !this.hasGitHubAccess(),
},
{
name: "Create/Update PR",
value: "pr",
disabled: !this.hasGitHubAccess() || !options.branchExists,
},
{
name: "Export as patch",
value: "export",
},
{
name: "Discard changes",
value: "discard",
},
];
return await inquirer.prompt([
{
type: "list",
name: "action",
message: "What would you like to do with your changes?",
choices,
},
]);
}
}
Phase 4: Security Measures
-
Token Isolation
- GitHub tokens NEVER enter the container
- All push operations happen from host process
- Tokens retrieved just-in-time when needed
-
Repository Protection
- Host repository is never modified directly
- All operations go through shadow repository
- User explicitly approves each operation
-
Audit Trail
class GitAuditLog { log(operation: GitOperation) { // Log all git operations for transparency console.log(`[GIT] ${operation.type} on ${operation.branch}`); // Store in ~/.claude-sandbox/git-operations.log } }
Implementation Phases
Phase 1: Basic Commit Extraction (Week 1)
- Implement git bundle creation in container
- Create shadow repository manager
- Build commit extraction pipeline
- Add commit review UI
Phase 2: Push Functionality (Week 2)
- Implement GitHub token discovery
- Add push to shadow repository
- Create push confirmation UI
- Handle push errors gracefully
Phase 3: PR Management (Week 3)
- Integrate with GitHub API
- Check for existing PRs
- Create/update PR functionality
- Add PR template support
Phase 4: Enhanced UX (Week 4)
- Add commit message editing
- Implement patch export option
- Create web UI for git operations
- Add git operation history
File Structure
claude-code-sandbox/
├── src/
│ ├── git/
│ │ ├── shadow-repository.ts
│ │ ├── git-operations-handler.ts
│ │ ├── github-integration.ts
│ │ └── git-audit-log.ts
│ ├── ui/
│ │ ├── session-end-ui.ts
│ │ └── git-review-ui.ts
│ └── credentials/
│ └── github-token-provider.ts
Example Usage Flow
-
Claude makes commits in container
# Inside container git commit -m "Add new feature" git commit -m "Fix bug"
-
Session ends, UI appears
🔔 Claude made 2 commits. What would you like to do? ❯ Review commits Push to branch 'claude/2025-01-27-feature' Create PR from 'claude/2025-01-27-feature' Export as patch Discard changes
-
User selects "Push to branch"
🔐 Authenticating with GitHub... ✓ Found GitHub token 📤 Pushing 2 commits to 'claude/2025-01-27-feature' ✓ Successfully pushed to GitHub 🔗 View branch: https://github.com/user/repo/tree/claude/2025-01-27-feature
Benefits
- Security: GitHub tokens never enter container
- Safety: Original repository untouched
- Flexibility: Multiple export options
- Transparency: All operations logged
- Seamless UX: Feels like normal git workflow
Technical Considerations
-
Bundle Limitations
- Bundles only contain commit objects
- Large binary files may need special handling
-
Shadow Repository Efficiency
- Only clones the specific branch Claude is working on
- Shallow clone (--depth 1) to minimize data transfer
- Bare repository (no working tree) saves disk space
- Temporary repos cleaned immediately after use
- For large repos, can use partial clone:
--filter=blob:none
-
Minimal Shadow Repo Approach
# Ultra-minimal: Just enough to receive and push commits git init --bare /tmp/shadow git -C /tmp/shadow remote add origin <repo-url> git -C /tmp/shadow fetch <bundle> <branch>:<branch> git -C /tmp/shadow push origin <branch>
-
Error Handling
- Network failures during push
- Merge conflicts detection
- Token expiration handling
Alternative Approaches Considered
- Git Worktree: Too complex, modifies host repo
- Direct Push from Container: Security risk
- Manual Patch Export: Poor UX
- Shared Volume: Risky, could corrupt host repo
Next Steps
- Prototype git bundle extraction
- Test shadow repository approach
- Build minimal UI for testing
- Gather feedback on UX flow