├── test ├── dummy.test.js ├── e2e │ ├── dummy-repo │ │ ├── docs │ │ │ └── example.md │ │ ├── README.md │ │ ├── src │ │ │ ├── utils.js │ │ │ └── main.js │ │ ├── package.json │ │ └── test │ │ │ └── test.js │ ├── run-tests.sh │ ├── simple-deletion-test.js │ ├── README.md │ ├── core-functionality-test.js │ ├── repo-to-container-sync-test.js │ ├── test-suite.js │ └── sync-test-framework.js ├── README.md ├── integration │ └── podman-support.test.js └── docker-config.test.js ├── tsconfig.json ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .eslintrc.js ├── claude-sandbox.config.example.json ├── jest.config.js ├── src ├── types.ts ├── config.ts ├── docker-config.ts ├── ui.ts ├── git-monitor.ts ├── credentials.ts ├── web-server-attach.ts └── index.ts ├── .claude └── settings.json ├── TODO.md ├── docs ├── claude-code-docs │ ├── Core tasks and workflows.md │ ├── Getting started with Claude Code.md │ ├── Manage costs effectively.md │ ├── Manage Claude's memory.md │ ├── IDE integrations.md │ ├── Claude Code overview.md │ ├── Troubleshooting.md │ ├── Manage permissions and security.md │ ├── SDK.md │ ├── Bedrock, Vertex, and proxies.md │ ├── Monitoring usage.md │ └── CLI usage and controls.md ├── lift-and-shift-credentials.md ├── setup-commands.md ├── github-authentication.md ├── environment-variables.md ├── 2025-05-25-first-design.md └── git-operations-plan.md ├── package.json ├── .gitignore ├── docker └── Dockerfile └── CLAUDE.md /test/dummy.test.js: -------------------------------------------------------------------------------- 1 | describe("Dummy Test", () => { 2 | it("should pass", () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/e2e/dummy-repo/docs/example.md: -------------------------------------------------------------------------------- 1 | # Example Documentation 2 | 3 | This is an example documentation file to ensure the docs directory is included in the test repository. 4 | -------------------------------------------------------------------------------- /test/e2e/dummy-repo/README.md: -------------------------------------------------------------------------------- 1 | # Test Repository 2 | 3 | This is a dummy repository for testing claude-sandbox file synchronization functionality. 4 | 5 | ## Files 6 | 7 | - `src/main.js` - Main application file 8 | - `src/utils.js` - Utility functions 9 | - `package.json` - Package configuration 10 | - `test/test.js` - Test file 11 | -------------------------------------------------------------------------------- /test/e2e/dummy-repo/src/utils.js: -------------------------------------------------------------------------------- 1 | function calculate(a, b) { 2 | return a + b; 3 | } 4 | 5 | function multiply(a, b) { 6 | return a * b; 7 | } 8 | 9 | function divide(a, b) { 10 | if (b === 0) { 11 | throw new Error("Division by zero"); 12 | } 13 | return a / b; 14 | } 15 | 16 | module.exports = { 17 | calculate, 18 | multiply, 19 | divide, 20 | }; 21 | -------------------------------------------------------------------------------- /test/e2e/dummy-repo/src/main.js: -------------------------------------------------------------------------------- 1 | const utils = require("./utils"); 2 | 3 | function main() { 4 | console.log("Starting test application..."); 5 | const result = utils.calculate(10, 5); 6 | console.log("Calculation result:", result); 7 | console.log("Application finished."); 8 | } 9 | 10 | if (require.main === module) { 11 | main(); 12 | } 13 | 14 | module.exports = { main }; 15 | -------------------------------------------------------------------------------- /test/e2e/dummy-repo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-repo", 3 | "version": "1.0.0", 4 | "description": "Test repository for claude-sandbox", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "test": "node test/test.js", 8 | "start": "node src/main.js" 9 | }, 10 | "keywords": [ 11 | "test", 12 | "claude", 13 | "sandbox" 14 | ], 15 | "author": "Test", 16 | "license": "MIT" 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "lib": ["ES2022"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x, 22.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: "npm" 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Build 29 | run: npm run build 30 | 31 | - name: Test 32 | run: npm test 33 | -------------------------------------------------------------------------------- /test/e2e/dummy-repo/test/test.js: -------------------------------------------------------------------------------- 1 | const utils = require("../src/utils"); 2 | 3 | function runTests() { 4 | console.log("Running tests..."); 5 | 6 | // Test calculate function 7 | const result1 = utils.calculate(2, 3); 8 | console.assert(result1 === 5, "Calculate test failed"); 9 | console.log("✓ Calculate test passed"); 10 | 11 | // Test multiply function 12 | const result2 = utils.multiply(4, 3); 13 | console.assert(result2 === 12, "Multiply test failed"); 14 | console.log("✓ Multiply test passed"); 15 | 16 | // Test divide function 17 | const result3 = utils.divide(10, 2); 18 | console.assert(result3 === 5, "Divide test failed"); 19 | console.log("✓ Divide test passed"); 20 | 21 | console.log("All tests passed!"); 22 | } 23 | 24 | runTests(); 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 4 | parserOptions: { 5 | ecmaVersion: 2022, 6 | sourceType: "module", 7 | project: "./tsconfig.json", 8 | }, 9 | env: { 10 | node: true, 11 | es2022: true, 12 | jest: true, 13 | }, 14 | plugins: ["@typescript-eslint"], 15 | rules: { 16 | "@typescript-eslint/explicit-module-boundary-types": "off", 17 | "@typescript-eslint/no-explicit-any": "warn", 18 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 19 | "no-console": "off", 20 | semi: ["error", "always"], 21 | quotes: ["error", "single"], 22 | "comma-dangle": ["error", "never"], 23 | }, 24 | ignorePatterns: ["dist/", "node_modules/", "coverage/", "*.js"], 25 | }; 26 | -------------------------------------------------------------------------------- /claude-sandbox.config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "dockerImage": "claude-code-sandbox:latest", 3 | "dockerfile": null, 4 | "detached": false, 5 | "autoPush": true, 6 | "autoCreatePR": true, 7 | "autoStartClaude": true, 8 | "envFile": ".env", 9 | "environment": { 10 | "NODE_ENV": "development" 11 | }, 12 | "setupCommands": ["npm install", "npm run build"], 13 | "volumes": [], 14 | "mounts": [ 15 | { 16 | "source": "./data", 17 | "target": "/workspace/data", 18 | "readonly": false 19 | }, 20 | { 21 | "source": "/home/user/shared-configs", 22 | "target": "/configs", 23 | "readonly": true 24 | } 25 | ], 26 | "allowedTools": ["*"], 27 | "maxThinkingTokens": 100000, 28 | "bashTimeout": 600000, 29 | "containerPrefix": "claude-code-sandbox", 30 | "claudeConfigPath": "~/.claude.json", 31 | "dockerSocketPath": null 32 | } 33 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | roots: ["/test"], 5 | testMatch: [ 6 | "**/test/**/*.test.ts", 7 | "**/test/**/*.test.js", 8 | "**/test/**/*.spec.ts", 9 | "**/test/**/*.spec.js", 10 | ], 11 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 12 | transform: { 13 | "^.+\\.tsx?$": "ts-jest", 14 | "^.+\\.jsx?$": "babel-jest", 15 | }, 16 | collectCoverageFrom: [ 17 | "src/**/*.{ts,tsx}", 18 | "!src/**/*.d.ts", 19 | "!src/**/*.test.{ts,tsx}", 20 | "!src/**/*.spec.{ts,tsx}", 21 | ], 22 | coverageDirectory: "/coverage", 23 | coverageReporters: ["text", "lcov", "html"], 24 | testPathIgnorePatterns: ["/node_modules/", "/dist/"], 25 | moduleNameMapper: { 26 | "^@/(.*)$": "/src/$1", 27 | }, 28 | globals: { 29 | "ts-jest": { 30 | tsconfig: { 31 | esModuleInterop: true, 32 | allowJs: true, 33 | }, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface VolumeMount { 2 | source: string; 3 | target: string; 4 | readonly?: boolean; 5 | } 6 | 7 | export interface SandboxConfig { 8 | dockerImage?: string; 9 | dockerfile?: string; 10 | containerPrefix?: string; 11 | autoPush?: boolean; 12 | autoCreatePR?: boolean; 13 | autoStartClaude?: boolean; 14 | defaultShell?: "claude" | "bash"; 15 | claudeConfigPath?: string; 16 | setupCommands?: string[]; 17 | environment?: Record; 18 | envFile?: string; 19 | volumes?: string[]; 20 | mounts?: VolumeMount[]; 21 | allowedTools?: string[]; 22 | maxThinkingTokens?: number; 23 | bashTimeout?: number; 24 | includeUntracked?: boolean; 25 | targetBranch?: string; 26 | remoteBranch?: string; 27 | prNumber?: string; 28 | dockerSocketPath?: string; 29 | } 30 | 31 | export interface Credentials { 32 | claude?: { 33 | type: "api_key" | "oauth" | "bedrock" | "vertex"; 34 | value: string; 35 | region?: string; 36 | project?: string; 37 | }; 38 | github?: { 39 | token?: string; 40 | gitConfig?: string; 41 | }; 42 | } 43 | 44 | export interface CommitInfo { 45 | hash: string; 46 | author: string; 47 | date: string; 48 | message: string; 49 | files: string[]; 50 | } 51 | -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(mkdir:*)", 5 | "Bash(npm run build:*)", 6 | "Bash(grep:*)", 7 | "Bash(ls:*)", 8 | "Bash(docker logs:*)", 9 | "Bash(docker kill:*)", 10 | "Bash(docker rm:*)", 11 | "Bash(npx tsc:*)", 12 | "Bash(npm install:*)", 13 | "Bash(node:*)", 14 | "Bash(timeout:*)", 15 | "Bash(true)", 16 | "Bash(docker stop:*)", 17 | "Bash(mv:*)", 18 | "Bash(curl:*)", 19 | "WebFetch(domain:localhost)", 20 | "Bash(pkill:*)", 21 | "Bash(docker exec:*)", 22 | "Bash(npx ts-node:*)", 23 | "Bash(docker pull:*)", 24 | "Bash(rg:*)", 25 | "Bash(npm start)", 26 | "Bash(find:*)", 27 | "Bash(npm run lint)", 28 | "Bash(sed:*)", 29 | "Bash(npx claude-sandbox purge:*)", 30 | "Bash(docker cp:*)", 31 | "Bash(npm run test:e2e:*)", 32 | "Bash(gh pr list:*)", 33 | "Bash(kill:*)", 34 | "Bash(npm start:*)", 35 | "Bash(npm run purge-containers:*)", 36 | "Bash(claude-sandbox start:*)", 37 | "Bash(npm run lint)", 38 | "Bash(npx eslint:*)", 39 | "Bash(npm test:*)", 40 | "Bash(gh issue view:*)", 41 | "Bash(npm install)" 42 | ], 43 | "deny": [] 44 | }, 45 | "enableAllProjectMcpServers": false 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # Version tags: v1.0.0, v1.0.1, etc. 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: "20.x" 24 | registry-url: "https://registry.npmjs.org" 25 | cache: "npm" 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: Extract version from tag and update package.json 31 | run: | 32 | # Get the version from the tag (remove 'v' prefix) 33 | TAG_VERSION=${GITHUB_REF#refs/tags/v} 34 | echo "Tag version: $TAG_VERSION" 35 | 36 | # Update package.json with the version from the tag 37 | npm version $TAG_VERSION --no-git-tag-version 38 | echo "Version updated in package.json to: $TAG_VERSION" 39 | 40 | # Verify the change 41 | cat package.json | grep '"version"' 42 | 43 | - name: Build 44 | run: npm run build 45 | 46 | - name: Test 47 | run: npm test 48 | 49 | - name: Publish 50 | run: npm publish 51 | env: 52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] Fix terminal output 2 | - [x] There is too much orchestration overhead to manage locally, so we need a UI to control, view, and do stuff outside of the container. The best thing is probably to create a web UI to be run locally, and the user will be redirected to this UI when they start a web app locally. 3 | - [x] Flag for including vs not including non-tracked files 4 | - [x] Specify with a flag which branch to switch to on container start 5 | - [x] Create listener for Claude turn end, to commit, push and create a PR 6 | - [x] GH token should not be copied into the container, huge security risk 7 | - [ ] Being able to specify which setup commands to run before and after the copying of Git files. 8 | - [ ] Being able to specify a PR to checkout the sandbox from 9 | - [ ] Reattaching to a running or stopped sandbox consistently, without losing work 10 | - [ ] More consistent pushing of changes to GitHub 11 | - [ ] How should we deal with it if the local shadow repo falls behind the remote branch? Options: 12 | - a. Let user merge changes from remote to local: We would need to implement a conflict resolver somehow. 13 | - b. If conflicts arise, we could just block the operation and let user dump the current state in order not to lose work. This is the simplest option. 14 | - Either way, we need to think about how to apply new commits from the remote, because changes currently only flow from the sandbox to the shadow repo. 15 | - [ ] rsync, inotifywait, etc. should be included in the image, not installed in the fly 16 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import os from "os"; 4 | import { SandboxConfig } from "./types"; 5 | 6 | const DEFAULT_CONFIG: SandboxConfig = { 7 | dockerImage: "claude-code-sandbox:latest", 8 | autoPush: true, 9 | autoCreatePR: true, 10 | autoStartClaude: true, 11 | defaultShell: "claude", // Default to Claude mode for backward compatibility 12 | claudeConfigPath: path.join(os.homedir(), ".claude.json"), 13 | setupCommands: [], // Example: ["npm install", "pip install -r requirements.txt"] 14 | allowedTools: ["*"], // All tools allowed in sandbox 15 | includeUntracked: false, // Don't include untracked files by default 16 | // maxThinkingTokens: 100000, 17 | // bashTimeout: 600000, // 10 minutes 18 | }; 19 | 20 | export async function loadConfig(configPath: string): Promise { 21 | try { 22 | const fullPath = path.resolve(configPath); 23 | const configContent = await fs.readFile(fullPath, "utf-8"); 24 | const userConfig = JSON.parse(configContent); 25 | 26 | // Merge with defaults 27 | return { 28 | ...DEFAULT_CONFIG, 29 | ...userConfig, 30 | }; 31 | } catch (error) { 32 | // Config file not found or invalid, use defaults 33 | return DEFAULT_CONFIG; 34 | } 35 | } 36 | 37 | export async function saveConfig( 38 | config: SandboxConfig, 39 | configPath: string, 40 | ): Promise { 41 | const fullPath = path.resolve(configPath); 42 | await fs.writeFile(fullPath, JSON.stringify(config, null, 2)); 43 | } 44 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Test Directory Structure 2 | 3 | This directory contains all tests for the Claude Code Sandbox project. 4 | 5 | ## Directory Layout 6 | 7 | ``` 8 | test/ 9 | ├── unit/ # Unit tests for individual modules 10 | ├── integration/ # Integration tests for module interactions 11 | ├── e2e/ # End-to-end tests for full workflow scenarios 12 | ├── fixtures/ # Test data, mock responses, sample files 13 | └── helpers/ # Shared test utilities and helpers 14 | ``` 15 | 16 | ## Running Tests 17 | 18 | ```bash 19 | # Run all tests 20 | npm test 21 | 22 | # Run tests in watch mode 23 | npm run test:watch 24 | 25 | # Run only unit tests 26 | npm run test:unit 27 | 28 | # Run only integration tests 29 | npm run test:integration 30 | 31 | # Run only E2E tests 32 | npm run test:e2e 33 | 34 | # Run tests with coverage 35 | npm run test:coverage 36 | ``` 37 | 38 | ## Test Naming Conventions 39 | 40 | - Unit tests: `*.test.ts` or `*.spec.ts` 41 | - Test files should mirror the source structure 42 | - Example: `test/unit/container.test.ts` tests `src/container.ts` 43 | 44 | ## Writing Tests 45 | 46 | Tests are written using Jest with TypeScript support. The Jest configuration is in `jest.config.js` at the project root. 47 | 48 | ### Example Unit Test 49 | 50 | ```typescript 51 | import { someFunction } from "../../src/someModule"; 52 | 53 | describe("someFunction", () => { 54 | it("should do something", () => { 55 | const result = someFunction("input"); 56 | expect(result).toBe("expected output"); 57 | }); 58 | }); 59 | ``` 60 | 61 | ## E2E Tests 62 | 63 | End-to-end tests are located in `test/e2e/` and test the complete workflow of the CLI tool. These tests: 64 | 65 | - Create actual Docker containers 66 | - Run Claude commands 67 | - Verify git operations 68 | - Test the full user experience 69 | 70 | Run E2E tests with: `npm run test:e2e` 71 | -------------------------------------------------------------------------------- /src/docker-config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | interface DockerConfig { 5 | socketPath?: string; 6 | } 7 | 8 | /** 9 | * Detects whether Docker or Podman is available and returns appropriate configuration 10 | * @param customSocketPath - Optional custom socket path from configuration 11 | */ 12 | export function getDockerConfig(customSocketPath?: string): DockerConfig { 13 | // Allow override via environment variable 14 | if (process.env.DOCKER_HOST) { 15 | return {}; // dockerode will use DOCKER_HOST automatically 16 | } 17 | 18 | // Use custom socket path if provided 19 | if (customSocketPath) { 20 | return { socketPath: customSocketPath }; 21 | } 22 | 23 | // Common socket paths to check 24 | const socketPaths = [ 25 | // Docker socket paths 26 | "/var/run/docker.sock", 27 | 28 | // Podman rootless socket paths 29 | process.env.XDG_RUNTIME_DIR && 30 | path.join(process.env.XDG_RUNTIME_DIR, "podman", "podman.sock"), 31 | `/run/user/${process.getuid?.() || 1000}/podman/podman.sock`, 32 | 33 | // Podman root socket path 34 | "/run/podman/podman.sock", 35 | ].filter(Boolean) as string[]; 36 | 37 | // Check each socket path 38 | for (const socketPath of socketPaths) { 39 | try { 40 | if (fs.existsSync(socketPath)) { 41 | const stats = fs.statSync(socketPath); 42 | if (stats.isSocket()) { 43 | return { socketPath }; 44 | } 45 | } 46 | } catch (error) { 47 | // Socket might exist but not be accessible, continue checking 48 | continue; 49 | } 50 | } 51 | 52 | // No socket found, return empty config and let dockerode use its defaults 53 | return {}; 54 | } 55 | 56 | /** 57 | * Checks if we're using Podman based on the socket path 58 | */ 59 | export function isPodman(config: DockerConfig): boolean { 60 | return config.socketPath?.includes("podman") ?? false; 61 | } 62 | -------------------------------------------------------------------------------- /docs/claude-code-docs/Core tasks and workflows.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Core tasks and workflows" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/common-tasks" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Explore Claude Code's powerful features for editing, searching, testing, and automating your development workflow." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | Claude Code operates directly in your terminal, understanding your project context and taking real actions. No need to manually add files to context - Claude will explore your codebase as needed. 14 | 15 | ## Understand unfamiliar code 16 | 17 | ## Automate Git operations 18 | 19 | ## Edit code intelligently 20 | 21 | ## Test and debug your code 22 | 23 | ## Encourage deeper thinking 24 | 25 | For complex problems, explicitly ask Claude to think more deeply: 26 | 27 | Claude Code will show when the model is using extended thinking. You can proactively prompt Claude to “think” or “think deeply” for more planning-intensive tasks. We suggest that you first tell Claude about your task and let it gather context from your project. Then, ask it to “think” to create a plan. 28 | 29 | Claude will think more based on the words you use. For example, “think hard” will trigger more extended thinking than saying “think” alone. 30 | 31 | For more tips, see [Extended thinking tips](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/extended-thinking-tips). 32 | 33 | ## Automate CI and infra workflows 34 | 35 | Claude Code comes with a non-interactive mode for headless execution. This is especially useful for running Claude Code in non-interactive contexts like scripts, pipelines, and Github Actions. 36 | 37 | Use `--print` (`-p`) to run Claude in non-interactive mode. In this mode, you can set the `ANTHROPIC_API_KEY` environment variable to provide a custom API key. 38 | 39 | Non-interactive mode is especially useful when you pre-configure the set of commands Claude is allowed to use: 40 | -------------------------------------------------------------------------------- /docs/claude-code-docs/Getting started with Claude Code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting started with Claude Code" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/getting-started" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Learn how to install, authenticate, and start using Claude Code." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | ## Check system requirements 14 | 15 | - **Operating Systems**: macOS 10.15+, Ubuntu 20.04+/Debian 10+, or Windows via WSL 16 | - **Hardware**: 4GB RAM minimum 17 | - **Software**: 18 | - Node.js 18+ 19 | - [git](https://git-scm.com/downloads) 2.23+ (optional) 20 | - [GitHub](https://cli.github.com/) or [GitLab](https://gitlab.com/gitlab-org/cli) CLI for PR workflows (optional) 21 | - [ripgrep](https://github.com/BurntSushi/ripgrep?tab=readme-ov-file#installation) (rg) for enhanced file search (optional) 22 | - **Network**: Internet connection required for authentication and AI processing 23 | - **Location**: Available only in [supported countries](https://www.anthropic.com/supported-countries) 24 | 25 | **Troubleshooting WSL installation** 26 | 27 | Currently, Claude Code does not run directly in Windows, and instead requires WSL. If you encounter issues in WSL: 28 | 29 | 1. **OS/platform detection issues**: If you receive an error during installation, WSL may be using Windows `npm`. Try: 30 | - Run `npm config set os linux` before installation 31 | - Install with `npm install -g @anthropic-ai/claude-code --force --no-os-check` (Do NOT use `sudo`) 32 | 2. **Node not found errors**: If you see `exec: node: not found` when running `claude`, your WSL environment may be using a Windows installation of Node.js. You can confirm this with `which npm` and `which node`, which should point to Linux paths starting with `/usr/` rather than `/mnt/c/`. To fix this, try installing Node via your Linux distribution’s package manager or via [`nvm`](https://github.com/nvm-sh/nvm). 33 | 34 | ## Install and authenticate 35 | 36 | ## Initialize your project 37 | 38 | For first-time users, we recommend: 39 | 40 | Getting started with Claude Code - Anthropic 41 | -------------------------------------------------------------------------------- /test/e2e/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # E2E Test Runner for Claude Sandbox File Sync 4 | # This script runs end-to-end tests to verify file synchronization functionality 5 | 6 | set -e 7 | 8 | echo "🚀 Claude Sandbox E2E Test Suite" 9 | echo "=================================" 10 | 11 | # Check dependencies 12 | echo "🔍 Checking dependencies..." 13 | if ! command -v node &> /dev/null; then 14 | echo "❌ Node.js is required but not installed" 15 | exit 1 16 | fi 17 | 18 | if ! command -v docker &> /dev/null; then 19 | echo "❌ Docker is required but not installed" 20 | exit 1 21 | fi 22 | 23 | if ! command -v npx &> /dev/null; then 24 | echo "❌ npx is required but not installed" 25 | exit 1 26 | fi 27 | 28 | echo "✅ Dependencies OK" 29 | 30 | # Clean up any existing containers 31 | echo "🧹 Cleaning up existing containers..." 32 | npx claude-sandbox purge -y || echo "No containers to clean up" 33 | 34 | # Run repository to container sync test 35 | echo "🧪 Running repository to container sync test..." 36 | cd "$(dirname "$0")" 37 | 38 | if node repo-to-container-sync-test.js; then 39 | echo "✅ Repository to container sync test passed" 40 | else 41 | echo "❌ Repository to container sync test failed" 42 | exit 1 43 | fi 44 | 45 | # Run core functionality tests 46 | echo "🧪 Running core functionality tests..." 47 | 48 | if node core-functionality-test.js; then 49 | echo "✅ Core functionality tests passed" 50 | else 51 | echo "❌ Core functionality tests failed" 52 | exit 1 53 | fi 54 | 55 | # Clean up after tests 56 | echo "🧹 Final cleanup..." 57 | npx claude-sandbox purge -y || echo "No containers to clean up" 58 | 59 | echo "🎉 All tests completed successfully!" 60 | echo "" 61 | echo "📝 Available tests:" 62 | echo " - node repo-to-container-sync-test.js (Verify one-to-one repo sync)" 63 | echo " - node core-functionality-test.js (Core file sync functionality)" 64 | echo " - node simple-deletion-test.js (Focused deletion test)" 65 | echo " - node test-suite.js (Full comprehensive test suite)" 66 | echo "" 67 | echo "✅ File synchronization functionality is working correctly!" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@textcortex/claude-code-sandbox", 3 | "version": "0.1.0", 4 | "description": "Run Claude Code as an autonomous agent in Docker containers with git integration", 5 | "main": "dist/index.js", 6 | "bin": { 7 | "claude-sandbox": "./dist/cli.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "dev": "tsc --watch", 12 | "start": "node dist/cli.js", 13 | "lint": "eslint src/**/*.ts", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "test:unit": "jest test/unit", 17 | "test:integration": "jest test/integration", 18 | "test:e2e": "cd test/e2e && ./run-tests.sh", 19 | "test:coverage": "jest --coverage", 20 | "purge-containers": "docker ps -a --filter \"ancestor=claude-code-sandbox:latest\" -q | xargs -r docker rm -f && docker rmi claude-code-sandbox:latest" 21 | }, 22 | "keywords": [ 23 | "claude", 24 | "ai", 25 | "docker", 26 | "sandbox", 27 | "automation" 28 | ], 29 | "author": "TextCortex Dev Team", 30 | "license": "MIT", 31 | "homepage": "https://github.com/textcortex/claude-code-sandbox#readme", 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/textcortex/claude-code-sandbox.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/textcortex/claude-code-sandbox/issues" 38 | }, 39 | "publishConfig": { 40 | "access": "public" 41 | }, 42 | "dependencies": { 43 | "@types/express": "^5.0.2", 44 | "@types/ws": "^8.18.1", 45 | "chalk": "^4.1.2", 46 | "chokidar": "^3.6.0", 47 | "commander": "^12.0.0", 48 | "dockerode": "^4.0.2", 49 | "express": "^5.1.0", 50 | "fs-extra": "^11.3.0", 51 | "inquirer": "^8.2.6", 52 | "open": "^10.1.2", 53 | "ora": "^5.4.1", 54 | "simple-git": "^3.22.0", 55 | "socket.io": "^4.8.1", 56 | "tar-stream": "^3.1.7", 57 | "ws": "^8.18.2", 58 | "xterm": "^5.3.0", 59 | "xterm-addon-fit": "^0.8.0", 60 | "xterm-addon-web-links": "^0.9.0" 61 | }, 62 | "devDependencies": { 63 | "@types/dockerode": "^3.3.23", 64 | "@types/fs-extra": "^11.0.4", 65 | "@types/inquirer": "^9.0.8", 66 | "@types/jest": "^29.5.12", 67 | "@types/node": "^20.11.5", 68 | "@types/puppeteer": "^5.4.7", 69 | "@types/socket.io-client": "^1.4.36", 70 | "@typescript-eslint/eslint-plugin": "^6.19.0", 71 | "@typescript-eslint/parser": "^6.19.0", 72 | "eslint": "^8.56.0", 73 | "jest": "^29.7.0", 74 | "puppeteer": "^24.9.0", 75 | "socket.io-client": "^4.8.1", 76 | "ts-jest": "^29.1.2", 77 | "typescript": "^5.3.3" 78 | }, 79 | "engines": { 80 | "node": ">=18.0.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/claude-code-docs/Manage costs effectively.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Manage costs effectively" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/costs" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Learn how to track and optimize token usage and costs when using Claude Code." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | Claude Code consumes tokens for each interaction. The average cost is $6 per developer per day, with daily costs remaining below $12 for 90% of users. 14 | 15 | ## Track your costs 16 | 17 | - Use `/cost` to see current session usage 18 | - **Anthropic Console users**: 19 | - Check [historical usage](https://support.anthropic.com/en/articles/9534590-cost-and-usage-reporting-in-console) in the Anthropic Console (requires Admin or Billing role) 20 | - Set [workspace spend limits](https://support.anthropic.com/en/articles/9796807-creating-and-managing-workspaces) for the Claude Code workspace (requires Admin role) 21 | - **Max plan users**: Usage is included in your Max plan subscription 22 | 23 | ## Reduce token usage 24 | 25 | - **Compact conversations:** 26 | - Claude uses auto-compact by default when context exceeds 95% capacity 27 | - Toggle auto-compact: Run `/config` and navigate to “Auto-compact enabled” 28 | - Use `/compact` manually when context gets large 29 | - Add custom instructions: `/compact Focus on code samples and API usage` 30 | - Customize compaction by adding to CLAUDE.md: 31 | - **Write specific queries:** Avoid vague requests that trigger unnecessary scanning 32 | - **Break down complex tasks:** Split large tasks into focused interactions 33 | - **Clear history between tasks:** Use `/clear` to reset context 34 | 35 | Costs can vary significantly based on: 36 | 37 | - Size of codebase being analyzed 38 | - Complexity of queries 39 | - Number of files being searched or modified 40 | - Length of conversation history 41 | - Frequency of compacting conversations 42 | - Background processes (haiku generation, conversation summarization) 43 | 44 | ## Background token usage 45 | 46 | Claude Code uses tokens for some background functionality even when idle: 47 | 48 | - **Haiku generation**: Small creative messages that appear while you type (approximately 1 cent per day) 49 | - **Conversation summarization**: Background jobs that summarize previous conversations for the `claude --resume` feature 50 | - **Command processing**: Some commands like `/cost` may generate requests to check status 51 | 52 | These background processes consume a small amount of tokens (typically under $0.04 per session) even without active interaction. 53 | 54 | For team deployments, we recommend starting with a small pilot group to establish usage patterns before wider rollout. 55 | -------------------------------------------------------------------------------- /test/e2e/simple-deletion-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { SyncTestFramework } = require("./sync-test-framework"); 4 | 5 | async function testDeletionFunctionality() { 6 | console.log("🧪 Testing File Deletion Functionality"); 7 | console.log("====================================="); 8 | 9 | const framework = new SyncTestFramework(); 10 | 11 | try { 12 | // Setup 13 | await framework.setup(); 14 | 15 | console.log("\n📝 Step 1: Adding test files..."); 16 | await framework.addFile("test1.txt", "Content 1"); 17 | await framework.addFile("test2.txt", "Content 2"); 18 | await framework.addFile("test3.txt", "Content 3"); 19 | await framework.waitForSync(); 20 | 21 | // Commit the files so deletions can be tracked 22 | const { stdout } = await require("util").promisify( 23 | require("child_process").exec, 24 | )( 25 | `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add test files"`, 26 | ); 27 | console.log("✅ Files committed to git"); 28 | 29 | console.log("\n🗑️ Step 2: Deleting one file..."); 30 | await framework.deleteFile("test2.txt"); 31 | await framework.waitForSync(); 32 | 33 | console.log("\n🔍 Step 3: Verifying deletion..."); 34 | 35 | // Check that file no longer exists in shadow repo 36 | const exists = await framework.shadowFileExists("test2.txt"); 37 | if (exists) { 38 | throw new Error("❌ File still exists in shadow repository"); 39 | } 40 | console.log("✅ File removed from shadow repository"); 41 | 42 | // Check git status shows deletion 43 | const gitStatus = await framework.getGitStatus(); 44 | console.log("Git status:", gitStatus); 45 | 46 | if (!gitStatus.includes("D test2.txt")) { 47 | throw new Error(`❌ Git status should show deletion: ${gitStatus}`); 48 | } 49 | console.log("✅ Git properly tracks deletion"); 50 | 51 | // Check other files still exist 52 | const exists1 = await framework.shadowFileExists("test1.txt"); 53 | const exists3 = await framework.shadowFileExists("test3.txt"); 54 | 55 | if (!exists1 || !exists3) { 56 | throw new Error("❌ Other files were incorrectly deleted"); 57 | } 58 | console.log("✅ Other files preserved"); 59 | 60 | console.log("\n🎉 SUCCESS: File deletion tracking is working correctly!"); 61 | } catch (error) { 62 | console.log(`\n❌ FAILED: ${error.message}`); 63 | throw error; 64 | } finally { 65 | await framework.cleanup(); 66 | } 67 | } 68 | 69 | testDeletionFunctionality() 70 | .then(() => { 71 | console.log("\n✅ Deletion test completed successfully"); 72 | process.exit(0); 73 | }) 74 | .catch((error) => { 75 | console.error("\n❌ Deletion test failed:", error.message); 76 | process.exit(1); 77 | }); 78 | -------------------------------------------------------------------------------- /src/ui.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import inquirer from "inquirer"; 3 | import ora from "ora"; 4 | import { CommitInfo } from "./types"; 5 | 6 | export class UIManager { 7 | showCommitNotification(commit: CommitInfo): void { 8 | console.log("\n" + chalk.yellow("━".repeat(80))); 9 | console.log(chalk.yellow.bold("🔔 New Commit Detected!")); 10 | console.log(chalk.yellow("━".repeat(80))); 11 | console.log(chalk.white(`Hash: ${commit.hash.substring(0, 8)}`)); 12 | console.log(chalk.white(`Author: ${commit.author}`)); 13 | console.log(chalk.white(`Date: ${commit.date}`)); 14 | console.log(chalk.white(`Message: ${commit.message}`)); 15 | console.log(chalk.white(`Files changed: ${commit.files.length}`)); 16 | commit.files.forEach((file) => { 17 | console.log(chalk.gray(` - ${file}`)); 18 | }); 19 | console.log(chalk.yellow("━".repeat(80)) + "\n"); 20 | } 21 | 22 | showDiff(diff: string): void { 23 | console.log(chalk.blue.bold("\n📝 Diff:")); 24 | console.log(chalk.blue("─".repeat(80))); 25 | 26 | // Apply syntax highlighting to diff 27 | const lines = diff.split("\n"); 28 | lines.forEach((line) => { 29 | if (line.startsWith("+") && !line.startsWith("+++")) { 30 | console.log(chalk.green(line)); 31 | } else if (line.startsWith("-") && !line.startsWith("---")) { 32 | console.log(chalk.red(line)); 33 | } else if (line.startsWith("@@")) { 34 | console.log(chalk.cyan(line)); 35 | } else if (line.startsWith("diff --git")) { 36 | console.log(chalk.yellow.bold(line)); 37 | } else { 38 | console.log(line); 39 | } 40 | }); 41 | 42 | console.log(chalk.blue("─".repeat(80)) + "\n"); 43 | } 44 | 45 | async askCommitAction(): Promise { 46 | const { action } = await inquirer.prompt([ 47 | { 48 | type: "list", 49 | name: "action", 50 | message: "What would you like to do?", 51 | choices: [ 52 | { name: "Continue working", value: "nothing" }, 53 | { name: "Push branch to remote", value: "push" }, 54 | { name: "Push branch and create PR", value: "push-pr" }, 55 | { name: "Exit claude-code-sandbox", value: "exit" }, 56 | ], 57 | }, 58 | ]); 59 | 60 | return action; 61 | } 62 | 63 | showSpinner(message: string): any { 64 | return ora(message).start(); 65 | } 66 | 67 | showSuccess(message: string): void { 68 | console.log(chalk.green(`✓ ${message}`)); 69 | } 70 | 71 | showError(message: string): void { 72 | console.log(chalk.red(`✗ ${message}`)); 73 | } 74 | 75 | showWarning(message: string): void { 76 | console.log(chalk.yellow(`⚠ ${message}`)); 77 | } 78 | 79 | showInfo(message: string): void { 80 | console.log(chalk.blue(`ℹ ${message}`)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .env 4 | .env.local 5 | *.log 6 | .DS_Store 7 | claude-sandbox.config.json 8 | !claude-sandbox.config.example.json 9 | reference-repos/ 10 | scratch/ 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | .pnpm-debug.log* 19 | 20 | # Diagnostic reports (https://nodejs.org/api/report.html) 21 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Directory for instrumented libs generated by jscoverage/JSCover 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | coverage 34 | *.lcov 35 | 36 | # nyc test coverage 37 | .nyc_output 38 | 39 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 40 | .grunt 41 | 42 | # Bower dependency directory (https://bower.io/) 43 | bower_components 44 | 45 | # node-waf configuration 46 | .lock-wscript 47 | 48 | # Compiled binary addons (https://nodejs.org/api/addons.html) 49 | build/Release 50 | 51 | # Dependency directories 52 | node_modules/ 53 | jspm_packages/ 54 | 55 | # Snowpack dependency directory (https://snowpack.dev/) 56 | web_modules/ 57 | 58 | # TypeScript cache 59 | *.tsbuildinfo 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Optional stylelint cache 68 | .stylelintcache 69 | 70 | # Microbundle cache 71 | .rpt2_cache/ 72 | .rts2_cache_cjs/ 73 | .rts2_cache_es/ 74 | .rts2_cache_umd/ 75 | 76 | # Optional REPL history 77 | .node_repl_history 78 | 79 | # Output of 'npm pack' 80 | *.tgz 81 | 82 | # Yarn Integrity file 83 | .yarn-integrity 84 | 85 | # dotenv environment variable files 86 | .env 87 | .env.development.local 88 | .env.test.local 89 | .env.production.local 90 | .env.local 91 | 92 | # parcel-bundler cache (https://parceljs.org/) 93 | .cache 94 | .parcel-cache 95 | 96 | # Next.js build output 97 | .next 98 | out 99 | 100 | # Nuxt.js build / generate output 101 | .nuxt 102 | dist 103 | 104 | # Gatsby files 105 | .cache/ 106 | # Comment in the public line in if your project uses Gatsby and not Next.js 107 | # https://nextjs.org/blog/next-9-1#public-directory-support 108 | # public 109 | 110 | # vuepress build output 111 | .vuepress/dist 112 | 113 | # vuepress v2.x temp and cache directory 114 | .temp 115 | .cache 116 | 117 | # vitepress build output 118 | **/.vitepress/dist 119 | 120 | # vitepress cache directory 121 | **/.vitepress/cache 122 | 123 | # Docusaurus cache and generated files 124 | .docusaurus 125 | 126 | # Serverless directories 127 | .serverless/ 128 | 129 | # FuseBox cache 130 | .fusebox/ 131 | 132 | # DynamoDB Local files 133 | .dynamodb/ 134 | 135 | # TernJS port file 136 | .tern-port 137 | 138 | # Stores VSCode versions used for testing VSCode extensions 139 | .vscode-test 140 | 141 | # yarn v2 142 | .yarn/cache 143 | .yarn/unplugged 144 | .yarn/build-state.yml 145 | .yarn/install-state.gz 146 | .pnp.* 147 | 148 | e2e-tests/dummy-repo/ -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | # Prevent interactive prompts during package installation 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | # Install system dependencies 7 | RUN apt-get update && apt-get install -y \ 8 | curl \ 9 | git \ 10 | openssh-client \ 11 | python3 \ 12 | python3-pip \ 13 | build-essential \ 14 | sudo \ 15 | vim \ 16 | jq \ 17 | ca-certificates \ 18 | gnupg \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | # Install Node.js 20.x 22 | RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ 23 | && apt-get install -y nodejs 24 | 25 | # Install GitHub CLI 26 | RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ 27 | && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ 28 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ 29 | && apt-get update \ 30 | && apt-get install -y gh 31 | 32 | # Install Claude Code 33 | RUN npm install -g @anthropic-ai/claude-code@latest 34 | 35 | # Create workspace directory 36 | RUN mkdir -p /workspace 37 | WORKDIR /workspace 38 | 39 | # Create a wrapper script for git that prevents branch switching 40 | RUN echo '#!/bin/bash\n\ 41 | # Allow the initial branch creation\n\ 42 | if [ ! -f /tmp/.branch-created ]; then\n\ 43 | /usr/bin/git.real "$@"\n\ 44 | if [[ "$1" == "checkout" ]] && [[ "$2" == "-b" ]]; then\n\ 45 | touch /tmp/.branch-created\n\ 46 | fi\n\ 47 | else\n\ 48 | # After initial branch creation, prevent switching\n\ 49 | if [[ "$1" == "checkout" ]] && [[ "$2" != "-b" ]] && [[ "$*" != *"--"* ]]; then\n\ 50 | echo "Error: Branch switching is disabled in claude-code-sandbox"\n\ 51 | echo "You can only create new branches with git checkout -b"\n\ 52 | exit 1\n\ 53 | fi\n\ 54 | if [[ "$1" == "switch" ]]; then\n\ 55 | echo "Error: Branch switching is disabled in claude-code-sandbox"\n\ 56 | exit 1\n\ 57 | fi\n\ 58 | /usr/bin/git.real "$@"\n\ 59 | fi' > /usr/local/bin/git-wrapper && \ 60 | chmod +x /usr/local/bin/git-wrapper && \ 61 | mv /usr/bin/git /usr/bin/git.real && \ 62 | ln -s /usr/local/bin/git-wrapper /usr/bin/git 63 | 64 | # Set up SSH config 65 | RUN mkdir -p /root/.ssh && \ 66 | echo "Host github.com\n\ 67 | StrictHostKeyChecking no\n\ 68 | UserKnownHostsFile /dev/null" > /root/.ssh/config && \ 69 | chmod 600 /root/.ssh/config 70 | 71 | # Set git to use credential helper 72 | RUN git config --global credential.helper 'cache --timeout=3600' 73 | 74 | # Create entrypoint script 75 | RUN echo '#!/bin/bash\n\ 76 | set -e\n\ 77 | \n\ 78 | # Copy SSH keys if mounted\n\ 79 | if [ -d "/tmp/.ssh" ]; then\n\ 80 | cp -r /tmp/.ssh/* /root/.ssh/ 2>/dev/null || true\n\ 81 | chmod 600 /root/.ssh/* 2>/dev/null || true\n\ 82 | fi\n\ 83 | \n\ 84 | # Copy git config if mounted\n\ 85 | if [ -f "/tmp/.gitconfig" ]; then\n\ 86 | cp /tmp/.gitconfig /root/.gitconfig\n\ 87 | fi\n\ 88 | \n\ 89 | # Execute the command\n\ 90 | exec "$@"' > /entrypoint.sh && \ 91 | chmod +x /entrypoint.sh 92 | 93 | ENTRYPOINT ["/entrypoint.sh"] 94 | CMD ["bash"] -------------------------------------------------------------------------------- /src/git-monitor.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGit } from "simple-git"; 2 | import { EventEmitter } from "events"; 3 | import chokidar from "chokidar"; 4 | import path from "path"; 5 | import { CommitInfo } from "./types"; 6 | 7 | export class GitMonitor extends EventEmitter { 8 | private git: SimpleGit; 9 | private watcher: chokidar.FSWatcher | null = null; 10 | private lastCommitHash: string = ""; 11 | private monitoring = false; 12 | 13 | constructor(git: SimpleGit) { 14 | super(); 15 | this.git = git; 16 | } 17 | 18 | async start(_branchName: string): Promise { 19 | this.monitoring = true; 20 | 21 | // Get initial commit hash 22 | const log = await this.git.log({ maxCount: 1 }); 23 | this.lastCommitHash = log.latest?.hash || ""; 24 | 25 | // Only watch if we're monitoring the host (for now we'll disable this) 26 | // TODO: This should watch the container's git directory instead 27 | console.log( 28 | "Note: Git monitoring is currently disabled for container isolation", 29 | ); 30 | return; 31 | 32 | // Watch .git directory for changes 33 | const gitDir = path.join(process.cwd(), ".git"); 34 | this.watcher = chokidar.watch(gitDir, { 35 | persistent: true, 36 | ignoreInitial: true, 37 | depth: 2, 38 | }); 39 | 40 | this.watcher!.on("change", async (filepath) => { 41 | if (!this.monitoring) return; 42 | 43 | // Check if there's a new commit 44 | if (filepath.includes("refs/heads") || filepath.includes("logs/HEAD")) { 45 | await this.checkForNewCommit(); 46 | } 47 | }); 48 | 49 | // Also poll periodically as backup 50 | this.startPolling(); 51 | } 52 | 53 | async stop(): Promise { 54 | this.monitoring = false; 55 | if (this.watcher) { 56 | await this.watcher.close(); 57 | this.watcher = null; 58 | } 59 | } 60 | 61 | private async checkForNewCommit(): Promise { 62 | try { 63 | const log = await this.git.log({ maxCount: 1 }); 64 | const latestHash = log.latest?.hash || ""; 65 | 66 | if (latestHash && latestHash !== this.lastCommitHash) { 67 | this.lastCommitHash = latestHash; 68 | 69 | // Get commit details 70 | const commit = await this.getCommitInfo(latestHash); 71 | this.emit("commit", commit); 72 | } 73 | } catch (error) { 74 | console.error("Error checking for new commit:", error); 75 | } 76 | } 77 | 78 | private async getCommitInfo(hash: string): Promise { 79 | const log = await this.git.log({ from: hash, to: hash, maxCount: 1 }); 80 | const commit = log.latest!; 81 | 82 | // Get list of changed files 83 | const diff = await this.git.diffSummary([`${hash}~1`, hash]); 84 | const files = diff.files.map((f) => f.file); 85 | 86 | return { 87 | hash: commit.hash, 88 | author: commit.author_name, 89 | date: commit.date, 90 | message: commit.message, 91 | files, 92 | }; 93 | } 94 | 95 | private startPolling(): void { 96 | const pollInterval = setInterval(async () => { 97 | if (!this.monitoring) { 98 | clearInterval(pollInterval); 99 | return; 100 | } 101 | await this.checkForNewCommit(); 102 | }, 2000); // Poll every 2 seconds 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /test/integration/podman-support.test.js: -------------------------------------------------------------------------------- 1 | const { loadConfig } = require('../../dist/config'); 2 | const { getDockerConfig, isPodman } = require('../../dist/docker-config'); 3 | const Docker = require('dockerode'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | describe('Podman Support Integration', () => { 8 | const testConfigPath = path.join(__dirname, 'test-podman-config.json'); 9 | 10 | afterEach(() => { 11 | // Clean up test config file 12 | if (fs.existsSync(testConfigPath)) { 13 | fs.unlinkSync(testConfigPath); 14 | } 15 | }); 16 | 17 | it('should load Podman socket path from configuration', async () => { 18 | // Create test config with Podman socket 19 | const testConfig = { 20 | dockerSocketPath: '/run/user/1000/podman/podman.sock', 21 | dockerImage: 'claude-code-sandbox:latest' 22 | }; 23 | fs.writeFileSync(testConfigPath, JSON.stringify(testConfig, null, 2)); 24 | 25 | // Load config 26 | const config = await loadConfig(testConfigPath); 27 | expect(config.dockerSocketPath).toBe('/run/user/1000/podman/podman.sock'); 28 | 29 | // Get Docker config 30 | const dockerConfig = getDockerConfig(config.dockerSocketPath); 31 | expect(dockerConfig.socketPath).toBe('/run/user/1000/podman/podman.sock'); 32 | expect(isPodman(dockerConfig)).toBe(true); 33 | }); 34 | 35 | it('should create Docker client with Podman socket', async () => { 36 | const podmanSocketPath = '/custom/podman/podman.sock'; 37 | const dockerConfig = getDockerConfig(podmanSocketPath); 38 | 39 | // Create Docker client 40 | const docker = new Docker(dockerConfig); 41 | 42 | // Verify the client has the correct socket path 43 | expect(docker.modem.socketPath).toBe(podmanSocketPath); 44 | }); 45 | 46 | it('should fallback to auto-detection when no socket path in config', async () => { 47 | // Create config without dockerSocketPath 48 | const testConfig = { 49 | dockerImage: 'claude-code-sandbox:latest' 50 | }; 51 | fs.writeFileSync(testConfigPath, JSON.stringify(testConfig, null, 2)); 52 | 53 | // Load config 54 | const config = await loadConfig(testConfigPath); 55 | expect(config.dockerSocketPath).toBeUndefined(); 56 | 57 | // Get Docker config - should auto-detect 58 | const dockerConfig = getDockerConfig(config.dockerSocketPath); 59 | 60 | // Should have detected something (Docker or Podman) 61 | if (dockerConfig.socketPath) { 62 | expect(typeof dockerConfig.socketPath).toBe('string'); 63 | } 64 | }); 65 | 66 | describe('Environment variable support', () => { 67 | let originalDockerHost; 68 | 69 | beforeEach(() => { 70 | originalDockerHost = process.env.DOCKER_HOST; 71 | }); 72 | 73 | afterEach(() => { 74 | if (originalDockerHost) { 75 | process.env.DOCKER_HOST = originalDockerHost; 76 | } else { 77 | delete process.env.DOCKER_HOST; 78 | } 79 | }); 80 | 81 | it('should respect DOCKER_HOST environment variable', () => { 82 | process.env.DOCKER_HOST = 'tcp://podman.local:2376'; 83 | 84 | const dockerConfig = getDockerConfig(); 85 | expect(dockerConfig).toEqual({}); 86 | 87 | // dockerode will handle DOCKER_HOST internally 88 | const docker = new Docker(dockerConfig); 89 | expect(docker.modem.host).toBe('podman.local'); 90 | expect(docker.modem.port).toBe('2376'); 91 | }); 92 | }); 93 | }); -------------------------------------------------------------------------------- /docs/lift-and-shift-credentials.md: -------------------------------------------------------------------------------- 1 | # Lift and Shift Credentials 2 | 3 | This document explains how `claude-sandbox` automatically transfers Claude credentials from your host machine to the Docker container. 4 | 5 | ## Overview 6 | 7 | Claude Code stores authentication credentials in different locations depending on your operating system and how you authenticated. The `claude-sandbox` tool automatically detects and copies these credentials to ensure Claude Code works seamlessly in the container. 8 | 9 | ## Credential Sources 10 | 11 | ### macOS Keychain (Priority 1) 12 | 13 | On macOS, Claude Code stores OAuth credentials in the system Keychain. These are automatically extracted using: 14 | 15 | ```bash 16 | security find-generic-password -s "Claude Code-credentials" -w 17 | ``` 18 | 19 | The credentials are stored as JSON: 20 | 21 | ```json 22 | { 23 | "claudeAiOauth": { 24 | "accessToken": "sk-ant-oat01-...", 25 | "refreshToken": "sk-ant-ort01-...", 26 | "expiresAt": 1748276587173, 27 | "scopes": ["user:inference", "user:profile"] 28 | } 29 | } 30 | ``` 31 | 32 | These credentials are copied to: `/home/claude/.claude/.credentials.json` 33 | 34 | ### API Key Configuration (Priority 2) 35 | 36 | If you have an API key stored in `~/.claude.json`: 37 | 38 | ```json 39 | { 40 | "api_key": "sk-ant-api03-..." 41 | } 42 | ``` 43 | 44 | This file is copied to: `/home/claude/.claude.json` 45 | 46 | ### Environment Variable (Priority 3) 47 | 48 | If `ANTHROPIC_API_KEY` is set in your environment, it's passed to the container: 49 | 50 | ```bash 51 | export ANTHROPIC_API_KEY="sk-ant-api03-..." 52 | ``` 53 | 54 | ### Existing .claude Directory (Fallback) 55 | 56 | On non-macOS systems, if `~/.claude/` directory exists, it's copied entirely to the container. 57 | 58 | ## File Permissions 59 | 60 | All copied credential files are set with appropriate permissions: 61 | 62 | - `.claude/` directory: `700` (owner read/write/execute only) 63 | - `.credentials.json`: `600` (owner read/write only) 64 | - `.claude.json`: `644` (owner read/write, others read) 65 | 66 | ## Security Considerations 67 | 68 | 1. **Keychain Access**: On macOS, you may be prompted to allow terminal access to your Keychain 69 | 2. **File Ownership**: All files are owned by the `claude` user in the container 70 | 3. **No Root Access**: Claude Code runs as a non-root user for security 71 | 4. **Credential Updates**: Changes to credentials in the container don't affect your host 72 | 73 | ## Troubleshooting 74 | 75 | ### macOS Keychain Access Denied 76 | 77 | If you see "No Claude credentials found in macOS Keychain", ensure: 78 | 79 | 1. You've logged into Claude Code on your host machine 80 | 2. Terminal has Keychain access permissions 81 | 3. The credential name is exactly "Claude Code-credentials" 82 | 83 | ### Missing Credentials 84 | 85 | If Claude Code prompts for login in the container: 86 | 87 | 1. Check if credentials exist on your host 88 | 2. Verify file permissions in the container 89 | 3. Try setting `ANTHROPIC_API_KEY` as a fallback 90 | 91 | ### Manual Credential Setup 92 | 93 | You can manually copy credentials into a running container: 94 | 95 | ```bash 96 | docker exec -it bash 97 | # Inside container: 98 | mkdir -p ~/.claude 99 | echo '{"api_key": "your-key"}' > ~/.claude.json 100 | ``` 101 | 102 | ## Platform Support 103 | 104 | - **macOS**: Full support with Keychain integration 105 | - **Linux**: Supports file-based credentials 106 | - **Windows**: Supports file-based credentials (WSL recommended) 107 | -------------------------------------------------------------------------------- /docs/claude-code-docs/Manage Claude's memory.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Manage Claude's memory" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/memory" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Learn how to manage Claude Code's memory across sessions with different memory locations and best practices." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | Claude Code can remember your preferences across sessions, like style guidelines and common commands in your workflow. 14 | 15 | ## Determine memory type 16 | 17 | Claude Code offers three memory locations, each serving a different purpose: 18 | 19 | | Memory Type | Location | Purpose | Use Case Examples | 20 | | -------------------------- | --------------------- | ---------------------------------------- | ---------------------------------------------------------------- | 21 | | **Project memory** | `./CLAUDE.md` | Team-shared instructions for the project | Project architecture, coding standards, common workflows | 22 | | **User memory** | `~/.claude/CLAUDE.md` | Personal preferences for all projects | Code styling preferences, personal tooling shortcuts | 23 | | **Project memory (local)** | `./CLAUDE.local.md` | Personal project-specific preferences | _(Deprecated, see below)_ Your sandbox URLs, preferred test data | 24 | 25 | All memory files are automatically loaded into Claude Code’s context when launched. 26 | 27 | ## CLAUDE.md imports 28 | 29 | CLAUDE.md files can import additional files using `@path/to/import` syntax. The following example imports 3 files: 30 | 31 | Both relative and absolute paths are allowed. In particular, importing files in user’s home dir is a convenient way for your team members to provide individual instructions that are not checked into the repository. Previously CLAUDE.local.md served a similar purpose, but is now deprecated in favor of imports since they work better across multiple git worktrees. 32 | 33 | To avoid potential collisions, imports are not evaluated inside markdown code spans and code blocks. 34 | 35 | Imported files can recursively import additional files, with a max-depth of 5 hops. You can see what memory files are loaded by running `/memory` command. 36 | 37 | ## How Claude looks up memories 38 | 39 | Claude Code reads memories recursively: starting in the cwd, Claude Code recurses up to _/_ and reads any CLAUDE.md or CLAUDE.local.md files it finds. This is especially convenient when working in large repositories where you run Claude Code in _foo/bar/_, and have memories in both _foo/CLAUDE.md_ and _foo/bar/CLAUDE.md_. 40 | 41 | Claude will also discover CLAUDE.md nested in subtrees under your current working directory. Instead of loading them at launch, they are only included when Claude reads files in those subtrees. 42 | 43 | ## Quickly add memories with the # shortcut 44 | 45 | The fastest way to add a memory is to start your input with the `#` character: 46 | 47 | You’ll be prompted to select which memory file to store this in. 48 | 49 | ## Directly edit memories with /memory 50 | 51 | Use the `/memory` slash command during a session to open any memory file in your system editor for more extensive additions or organization. 52 | 53 | ## Memory best practices 54 | 55 | - **Be specific**: “Use 2-space indentation” is better than “Format code properly”. 56 | - **Use structure to organize**: Format each individual memory as a bullet point and group related memories under descriptive markdown headings. 57 | - **Review periodically**: Update memories as your project evolves to ensure Claude is always using the most up to date information and context. 58 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Overview 6 | 7 | This is the Claude Code Sandbox project - a CLI tool that runs Claude Code instances inside isolated Docker containers with automatic git integration. The tool creates safe sandboxed environments where Claude can execute commands and make code changes without affecting the host system. 8 | 9 | ## Common Development Commands 10 | 11 | ### Build and Development 12 | 13 | - `npm run build` - Compile TypeScript to JavaScript (output in `dist/`) 14 | - `npm run dev` - Watch mode for TypeScript compilation 15 | - `npm start` - Run the CLI tool 16 | 17 | ### Testing and Quality 18 | 19 | - `npm run lint` - Run ESLint on TypeScript files 20 | - `npm test` - Run Jest tests 21 | 22 | ### Container Management 23 | 24 | - `npm run purge-containers` - Remove all Claude Sandbox containers and images 25 | 26 | ## Architecture 27 | 28 | ### Core Components 29 | 30 | 1. **CLI Entry Point** (`src/cli.ts`) 31 | 32 | - Command-line interface using Commander.js 33 | - Handles options parsing and main flow orchestration 34 | 35 | 2. **Container Management** (`src/container.ts`) 36 | 37 | - Docker container lifecycle management using dockerode 38 | - Builds images, creates containers, handles streams 39 | - Manages volume mounts for credentials and workspace 40 | 41 | 3. **Git Integration** (`src/git-monitor.ts`) 42 | 43 | - Monitors git repository for new commits 44 | - Uses simple-git for operations 45 | - Provides real-time notifications of Claude's commits 46 | 47 | 4. **Credential Discovery** (`src/credentials.ts`) 48 | 49 | - Automatically discovers Claude API keys (Anthropic, AWS Bedrock, Google Vertex) 50 | - Discovers GitHub credentials (CLI auth, SSH keys) 51 | - Mounts credentials read-only into containers 52 | 53 | 5. **Configuration** (`src/config.ts`) 54 | 55 | - Loads and validates configuration from `claude-sandbox.config.json` 56 | - Manages Docker settings, environment variables, and Claude parameters 57 | 58 | 6. **UI Components** (`src/ui.ts`) 59 | - Interactive prompts using inquirer 60 | - Diff display with syntax highlighting 61 | - Commit review interface 62 | 63 | ### Key Design Decisions 64 | 65 | - Claude runs with `--dangerously-skip-permissions` flag (safe within container isolation) 66 | - Git wrapper prevents branch switching to protect main branch 67 | - All credentials are mounted read-only 68 | - Each session creates a new branch (`claude/[timestamp]`) 69 | - Real-time commit monitoring with interactive review 70 | 71 | ### Shadow Repository Sync Principles 72 | 73 | The shadow repository maintains a real-time sync with the container's workspace using the following principles: 74 | 75 | 1. **Git-tracked files take precedence**: Any file that is committed to the git repository will be synced to the shadow repo, regardless of whether it matches patterns in `.gitignore` 76 | 2. **Gitignore patterns apply to untracked files**: Files that are not committed to git but match `.gitignore` patterns will be excluded from sync 77 | 3. **Built-in exclusions**: Certain directories (`.git`, `node_modules`, `__pycache__`, etc.) are always excluded for performance and safety 78 | 4. **Rsync rule order**: Include rules for git-tracked files are processed before exclude rules, ensuring committed files are always preserved 79 | 80 | This ensures that important data files (like corpora, model files, etc.) that are committed to the repository are never accidentally deleted during sync operations, even if they match common gitignore patterns like `*.zip` or `*.tar.gz`. 81 | 82 | ## Configuration 83 | 84 | The tool looks for `claude-sandbox.config.json` in the working directory. Key options: 85 | 86 | - `dockerImage`: Base image name 87 | - `dockerfile`: Path to custom Dockerfile 88 | - `environment`: Additional environment variables 89 | - `volumes`: Additional volume mounts 90 | - `allowedTools`: Claude tool permissions (default: all) 91 | - `autoPush`/`autoCreatePR`: Git workflow settings 92 | 93 | ## Development Workflow 94 | 95 | Start a new sandbox: 96 | 97 | ``` 98 | claude-sandbox start 99 | ``` 100 | 101 | Kill all running sandbox containers: 102 | 103 | ``` 104 | claude-sandbox purge -y 105 | ``` 106 | -------------------------------------------------------------------------------- /docs/claude-code-docs/IDE integrations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "IDE integrations" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/ide-integrations" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Integrate Claude Code with your favorite development environments" 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | Claude Code seamlessly integrates with popular Integrated Development Environments (IDEs) to enhance your coding workflow. This integration allows you to leverage Claude’s capabilities directly within your preferred development environment. 14 | 15 | ## Supported IDEs 16 | 17 | Claude Code currently supports two major IDE families: 18 | 19 | - **Visual Studio Code** (including popular forks like Cursor and Windsurf) 20 | - **JetBrains IDEs** (including PyCharm, WebStorm, IntelliJ, and GoLand) 21 | 22 | ## Features 23 | 24 | - **Quick launch**: Use `Cmd+Esc` (Mac) or `Ctrl+Esc` (Windows/Linux) to open Claude Code directly from your editor, or click the Claude Code button in the UI 25 | - **Diff viewing**: Code changes can be displayed directly in the IDE diff viewer instead of the terminal. You can configure this in `/config` 26 | - **Selection context**: The current selection/tab in the IDE is automatically shared with Claude Code 27 | - **File reference shortcuts**: Use `Cmd+Option+K` (Mac) or `Alt+Ctrl+K` (Linux/Windows) to insert file references (e.g., @File#L1-99) 28 | - **Diagnostic sharing**: Diagnostic errors (lint, syntax, etc.) from the IDE are automatically shared with Claude as you work 29 | 30 | ## Installation 31 | 32 | ### VS Code 33 | 34 | 1. Open VSCode 35 | 2. Open the integrated terminal 36 | 3. Run `claude` - the extension will auto-install 37 | 38 | Going forward you can also use the `/ide` command in any external terminal to connect to the IDE. 39 | 40 | These installation instructions also apply to VS Code forks like Cursor and Windsurf. 41 | 42 | ### JetBrains IDEs 43 | 44 | Install the [Claude Code plugin](https://docs.anthropic.com/s/claude-code-jetbrains) from the marketplace and restart your IDE. 45 | 46 | The plugin may also be auto-installed when you run `claude` in the integrated terminal. The IDE must be restarted completely to take effect. 47 | 48 | **Remote Development Limitations**: When using JetBrains Remote Development, you must install the plugin in the remote host via `Settings > Plugin (Host)`. 49 | 50 | ## Configuration 51 | 52 | Both integrations work with Claude Code’s configuration system. To enable IDE-specific features: 53 | 54 | 1. Connect Claude Code to your IDE by running `claude` in the built-in terminal 55 | 2. Run the `/config` command 56 | 3. Set the diff tool to `auto` for automatic IDE detection 57 | 4. Claude Code will automatically use the appropriate viewer based on your IDE 58 | 59 | If you’re using an external terminal (not the IDE’s built-in terminal), you can still connect to your IDE by using the `/ide` command after launching Claude Code. This allows you to benefit from IDE integration features even when running Claude from a separate terminal application. This works for both VS Code and JetBrains IDEs. 60 | 61 | When using an external terminal, to ensure Claude has default access to the same files as your IDE, start Claude from the same directory as your IDE project root. 62 | 63 | ## Troubleshooting 64 | 65 | ### VS Code extension not installing 66 | 67 | - Ensure you’re running Claude Code from VS Code’s integrated terminal 68 | - Ensure that the CLI corresponding to your IDE is installed: 69 | - For VS Code: `code` command should be available 70 | - For Cursor: `cursor` command should be available 71 | - For Windsurf: `windsurf` command should be available 72 | - If not installed, use `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) and search for “Shell Command: Install ‘code’ command in PATH” (or the equivalent for your IDE) 73 | - Check that VS Code has permission to install extensions 74 | 75 | ### JetBrains plugin not working 76 | 77 | - Ensure you’re running Claude Code from the project root directory 78 | - Check that the JetBrains plugin is enabled in the IDE settings 79 | - Completely restart the IDE. You may need to do this multiple times 80 | - For JetBrains Remote Development, ensure that the Claude Code plugin is installed in the remote host and not locally on the client 81 | 82 | For additional help, refer to our [troubleshooting guide](https://docs.anthropic.com/docs/claude-code/troubleshooting) or reach out to support. 83 | -------------------------------------------------------------------------------- /docs/setup-commands.md: -------------------------------------------------------------------------------- 1 | # Setup Commands 2 | 3 | This document explains how to run custom setup commands in your Claude Sandbox container. 4 | 5 | ## Overview 6 | 7 | Setup commands allow you to automatically run initialization scripts when your container starts. This is useful for: 8 | 9 | - Installing project dependencies 10 | - Setting up databases 11 | - Configuring environment-specific settings 12 | - Installing additional tools 13 | 14 | ## Configuration 15 | 16 | Add a `setupCommands` array to your `claude-sandbox.config.json`: 17 | 18 | ```json 19 | { 20 | "setupCommands": [ 21 | "npm install", 22 | "pip install -r requirements.txt", 23 | "sudo apt-get update && sudo apt-get install -y postgresql-client", 24 | "createdb myapp_dev || true" 25 | ] 26 | } 27 | ``` 28 | 29 | ## Execution Order 30 | 31 | Setup commands run: 32 | 33 | 1. **After** workspace files are copied 34 | 2. **After** git branch is created 35 | 3. **Before** Claude Code starts (if auto-start is enabled) 36 | 4. **As the `claude` user** (with sudo access) 37 | 38 | ## Examples 39 | 40 | ### Node.js Project 41 | 42 | ```json 43 | { 44 | "setupCommands": ["npm install", "npm run build", "npm run db:migrate"] 45 | } 46 | ``` 47 | 48 | ### Python Project 49 | 50 | ```json 51 | { 52 | "setupCommands": [ 53 | "pip install -r requirements.txt", 54 | "python manage.py migrate", 55 | "python manage.py collectstatic --noinput" 56 | ] 57 | } 58 | ``` 59 | 60 | ### Installing System Packages 61 | 62 | ```json 63 | { 64 | "setupCommands": [ 65 | "sudo apt-get update", 66 | "sudo apt-get install -y redis-server postgresql-client", 67 | "sudo service redis-server start" 68 | ] 69 | } 70 | ``` 71 | 72 | ### Complex Setup 73 | 74 | ```json 75 | { 76 | "setupCommands": [ 77 | "# Install dependencies", 78 | "npm install && pip install -r requirements.txt", 79 | 80 | "# Set up database", 81 | "sudo service postgresql start", 82 | "createdb myapp_dev || true", 83 | "npm run db:migrate", 84 | 85 | "# Start background services", 86 | "redis-server --daemonize yes", 87 | "npm run workers:start &" 88 | ] 89 | } 90 | ``` 91 | 92 | ## Best Practices 93 | 94 | 1. **Use `|| true`** for commands that might fail but shouldn't stop setup: 95 | 96 | ```json 97 | ["createdb myapp_dev || true"] 98 | ``` 99 | 100 | 2. **Chain related commands** with `&&`: 101 | 102 | ```json 103 | ["cd frontend && npm install && npm run build"] 104 | ``` 105 | 106 | 3. **Add comments** for clarity: 107 | 108 | ```json 109 | ["# Install Python dependencies", "pip install -r requirements.txt"] 110 | ``` 111 | 112 | 4. **Test commands** in a regular container first: 113 | ```bash 114 | docker run -it claude-code-sandbox:latest bash 115 | # Test your commands here 116 | ``` 117 | 118 | ## Error Handling 119 | 120 | - Commands are run sequentially 121 | - If a command fails (non-zero exit code), subsequent commands still run 122 | - Failed commands show an error message but don't stop the container 123 | - To stop on first error, add `"set -e"` as the first command 124 | 125 | ## Working Directory 126 | 127 | All commands run in `/workspace` (your project root) as the `claude` user. 128 | 129 | ## Environment Variables 130 | 131 | Commands have access to: 132 | 133 | - All environment variables from your config 134 | - Standard container environment 135 | - `HOME=/home/claude` 136 | - `USER=claude` 137 | 138 | ## Limitations 139 | 140 | - Commands run synchronously (one at a time) 141 | - Long-running commands will delay container startup 142 | - Background processes should be daemonized 143 | - Output is prefixed with `>` for clarity 144 | 145 | ## Troubleshooting 146 | 147 | ### Command Not Found 148 | 149 | Ensure the tool is installed in the Docker image or install it in your setup commands: 150 | 151 | ```json 152 | { 153 | "setupCommands": ["sudo apt-get update && sudo apt-get install -y "] 154 | } 155 | ``` 156 | 157 | ### Permission Denied 158 | 159 | The `claude` user has passwordless sudo access. Prefix commands with `sudo` if needed: 160 | 161 | ```json 162 | { 163 | "setupCommands": ["sudo systemctl start postgresql"] 164 | } 165 | ``` 166 | 167 | ### Command Hangs 168 | 169 | Ensure commands don't wait for user input. Use flags like `-y` or `--yes`: 170 | 171 | ```json 172 | { 173 | "setupCommands": ["sudo apt-get install -y package-name"] 174 | } 175 | ``` 176 | -------------------------------------------------------------------------------- /src/credentials.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import os from "os"; 4 | import { execSync } from "child_process"; 5 | import { Credentials } from "./types"; 6 | import chalk from "chalk"; 7 | 8 | export class CredentialManager { 9 | async discover(): Promise { 10 | const credentials: Credentials = {}; 11 | 12 | // Discover Claude credentials (optional) 13 | try { 14 | credentials.claude = await this.discoverClaudeCredentials(); 15 | } catch { 16 | // Claude credentials are optional - user can set them in the container 17 | console.log( 18 | chalk.yellow( 19 | "No Claude credentials found on host. You can set them in the container.", 20 | ), 21 | ); 22 | } 23 | 24 | // Discover GitHub credentials 25 | credentials.github = await this.discoverGitHubCredentials(); 26 | 27 | return credentials; 28 | } 29 | 30 | private async discoverClaudeCredentials(): Promise { 31 | // Check environment variables 32 | if (process.env.ANTHROPIC_API_KEY) { 33 | return { 34 | type: "api_key", 35 | value: process.env.ANTHROPIC_API_KEY, 36 | }; 37 | } 38 | 39 | // Check for ~/.claude.json configuration 40 | try { 41 | const claudeConfigPath = path.join(os.homedir(), ".claude.json"); 42 | const configContent = await fs.readFile(claudeConfigPath, "utf-8"); 43 | const config = JSON.parse(configContent); 44 | 45 | if (config.api_key) { 46 | return { 47 | type: "api_key", 48 | value: config.api_key, 49 | }; 50 | } 51 | } catch { 52 | // File doesn't exist or is invalid, continue checking other sources 53 | } 54 | 55 | // Check for Bedrock configuration 56 | if (process.env.CLAUDE_CODE_USE_BEDROCK === "1") { 57 | return { 58 | type: "bedrock", 59 | value: "bedrock", 60 | region: process.env.AWS_REGION || "us-east-1", 61 | }; 62 | } 63 | 64 | // Check for Vertex configuration 65 | if (process.env.CLAUDE_CODE_USE_VERTEX === "1") { 66 | return { 67 | type: "vertex", 68 | value: "vertex", 69 | project: process.env.GOOGLE_CLOUD_PROJECT, 70 | }; 71 | } 72 | 73 | // Try to find OAuth tokens (Claude Max) 74 | const oauthToken = await this.findOAuthToken(); 75 | if (oauthToken) { 76 | return { 77 | type: "oauth", 78 | value: oauthToken, 79 | }; 80 | } 81 | 82 | throw new Error( 83 | "No Claude credentials found. Please set ANTHROPIC_API_KEY or create ~/.claude.json with your API key.", 84 | ); 85 | } 86 | 87 | private async findOAuthToken(): Promise { 88 | // Check common locations for Claude OAuth tokens 89 | const possiblePaths = [ 90 | path.join(os.homedir(), ".claude", "auth.json"), 91 | path.join( 92 | os.homedir(), 93 | "Library", 94 | "Application Support", 95 | "Claude", 96 | "auth.json", 97 | ), 98 | path.join(os.homedir(), ".config", "claude", "auth.json"), 99 | ]; 100 | 101 | for (const authPath of possiblePaths) { 102 | try { 103 | const content = await fs.readFile(authPath, "utf-8"); 104 | const auth = JSON.parse(content); 105 | if (auth.access_token) { 106 | return auth.access_token; 107 | } 108 | } catch { 109 | // Continue checking other paths 110 | } 111 | } 112 | 113 | // Try to get from system keychain (macOS) 114 | if (process.platform === "darwin") { 115 | try { 116 | const token = execSync( 117 | 'security find-generic-password -s "claude-auth" -w 2>/dev/null', 118 | { 119 | encoding: "utf-8", 120 | }, 121 | ).trim(); 122 | if (token) return token; 123 | } catch { 124 | // Keychain access failed 125 | } 126 | } 127 | 128 | return null; 129 | } 130 | 131 | private async discoverGitHubCredentials(): Promise { 132 | const github: Credentials["github"] = {}; 133 | 134 | // Check for GitHub token in environment 135 | if (process.env.GITHUB_TOKEN) { 136 | github.token = process.env.GITHUB_TOKEN; 137 | } else if (process.env.GH_TOKEN) { 138 | github.token = process.env.GH_TOKEN; 139 | } else { 140 | // Try to get from gh CLI 141 | try { 142 | const token = execSync("gh auth token 2>/dev/null", { 143 | encoding: "utf-8", 144 | }).trim(); 145 | if (token) github.token = token; 146 | } catch { 147 | // gh CLI not available or not authenticated 148 | } 149 | } 150 | 151 | // Get git config 152 | try { 153 | const gitConfig = await fs.readFile( 154 | path.join(os.homedir(), ".gitconfig"), 155 | "utf-8", 156 | ); 157 | github.gitConfig = gitConfig; 158 | } catch { 159 | // No git config found 160 | } 161 | 162 | return github; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /docs/github-authentication.md: -------------------------------------------------------------------------------- 1 | # GitHub Authentication 2 | 3 | This document explains how to set up GitHub authentication for use within Claude Sandbox containers. 4 | 5 | ## Overview 6 | 7 | Claude Sandbox uses GitHub tokens for authentication, providing a secure and simple way to access private repositories and push changes. 8 | 9 | ## Authentication Methods (in order of preference) 10 | 11 | ### 1. GitHub CLI Token (Recommended) 12 | 13 | The most secure and convenient method: 14 | 15 | ```bash 16 | # One-time setup on host: 17 | gh auth login 18 | 19 | # The token is automatically discovered and passed to containers 20 | ``` 21 | 22 | **How it works:** 23 | 24 | - Claude Sandbox runs `gh auth token` to get your token 25 | - Token is passed as `GITHUB_TOKEN` environment variable 26 | - Git is configured to use the token for both HTTPS and SSH URLs 27 | - Works for cloning, pulling, and pushing 28 | 29 | **Benefits:** 30 | 31 | - ✅ Cross-platform (macOS, Linux, Windows) 32 | - ✅ Secure (tokens can be scoped) 33 | - ✅ Easy to refresh (`gh auth refresh`) 34 | - ✅ No manual token management 35 | 36 | ### 2. Environment Variables 37 | 38 | Set a token in your shell: 39 | 40 | ```bash 41 | # Using GitHub Personal Access Token 42 | export GITHUB_TOKEN=ghp_xxxxxxxxxxxx 43 | 44 | # Or using GitHub CLI token 45 | export GH_TOKEN=$(gh auth token) 46 | 47 | # Then run 48 | claude-sandbox 49 | ``` 50 | 51 | **Supported variables:** 52 | 53 | - `GITHUB_TOKEN` - Standard GitHub token variable 54 | - `GH_TOKEN` - GitHub CLI token variable 55 | 56 | ### 3. Git Configuration 57 | 58 | Your `.gitconfig` is automatically copied to containers, preserving: 59 | 60 | - User name and email 61 | - Custom aliases 62 | - Other git settings (excluding credential helpers) 63 | 64 | ## Setup Examples 65 | 66 | ### Quick Setup (GitHub CLI) 67 | 68 | ```bash 69 | # Install GitHub CLI 70 | brew install gh # macOS 71 | # or 72 | sudo apt install gh # Ubuntu/Debian 73 | 74 | # Authenticate 75 | gh auth login 76 | 77 | # Run claude-sandbox (token is auto-detected) 78 | claude-sandbox 79 | ``` 80 | 81 | ### Manual Token Setup 82 | 83 | 1. Create a Personal Access Token: 84 | 85 | - Go to GitHub Settings → Developer settings → Personal access tokens 86 | - Create a token with `repo` scope 87 | - Copy the token 88 | 89 | 2. Set environment variable: 90 | ```bash 91 | export GITHUB_TOKEN=ghp_your_token_here 92 | claude-sandbox 93 | ``` 94 | 95 | ## Using in Container 96 | 97 | Once authenticated, git is automatically configured to use your token: 98 | 99 | ```bash 100 | # Clone private repos (both HTTPS and SSH URLs work) 101 | git clone https://github.com/username/private-repo.git 102 | git clone git@github.com:username/private-repo.git 103 | 104 | # Use GitHub CLI 105 | gh repo create 106 | gh pr create 107 | gh issue list 108 | 109 | # Push changes 110 | git push origin main 111 | ``` 112 | 113 | ## Configuration File 114 | 115 | Add GitHub token to your project's `claude-sandbox.config.json`: 116 | 117 | ```json 118 | { 119 | "environment": { 120 | "GITHUB_TOKEN": "ghp_xxxxxxxxxxxx" 121 | } 122 | } 123 | ``` 124 | 125 | **Warning:** Don't commit tokens to version control! 126 | 127 | ## Troubleshooting 128 | 129 | ### Permission Denied 130 | 131 | If you get "Permission denied" errors: 132 | 133 | 1. Check if token is available: 134 | 135 | ```bash 136 | # In container 137 | echo $GITHUB_TOKEN 138 | gh auth status 139 | ``` 140 | 141 | 2. Verify git configuration: 142 | ```bash 143 | git config --list | grep url 144 | ``` 145 | 146 | ### Token Not Found 147 | 148 | If no token is detected: 149 | 150 | - Ensure you're logged in with `gh auth login` 151 | - Or set `GITHUB_TOKEN` environment variable 152 | - Check that the token has appropriate scopes 153 | 154 | ### Rate Limiting 155 | 156 | If you hit rate limits: 157 | 158 | - Ensure you're using an authenticated token 159 | - Check rate limit: `gh api rate_limit` 160 | 161 | ## Security Best Practices 162 | 163 | 1. **Use Scoped Tokens**: Only grant necessary permissions (usually just `repo`) 164 | 2. **Rotate Tokens**: Regularly refresh tokens 165 | 3. **Don't Commit Tokens**: Use environment variables 166 | 4. **Use GitHub CLI**: It manages token lifecycle automatically 167 | 168 | ## Platform-Specific Notes 169 | 170 | ### macOS 171 | 172 | - GitHub CLI token stored in macOS Keychain 173 | - Git credentials may use osxkeychain helper 174 | 175 | ### Linux 176 | 177 | - GitHub CLI token in `~/.config/gh/` 178 | - Git credentials may use libsecret 179 | 180 | ### Windows (WSL) 181 | 182 | - Use WSL for best compatibility 183 | - GitHub CLI works in WSL 184 | 185 | ## Advanced Configuration 186 | 187 | ### Multiple GitHub Accounts 188 | 189 | Use different tokens for different organizations: 190 | 191 | ```bash 192 | # For work repos 193 | export GITHUB_TOKEN=ghp_work_token 194 | 195 | # For personal repos (in another session) 196 | export GITHUB_TOKEN=ghp_personal_token 197 | ``` 198 | 199 | ### Custom Git Configuration 200 | 201 | The container automatically configures git to use tokens for all GitHub URLs: 202 | 203 | - `https://github.com/` URLs use token authentication 204 | - `git@github.com:` URLs are rewritten to use HTTPS with token 205 | 206 | This means you can clone repositories using either format and authentication will work seamlessly. 207 | -------------------------------------------------------------------------------- /test/docker-config.test.js: -------------------------------------------------------------------------------- 1 | const { getDockerConfig, isPodman } = require('../dist/docker-config'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | describe('Docker/Podman Configuration', () => { 6 | let originalEnv; 7 | 8 | beforeEach(() => { 9 | // Save original environment 10 | originalEnv = { ...process.env }; 11 | }); 12 | 13 | afterEach(() => { 14 | // Restore original environment 15 | process.env = originalEnv; 16 | }); 17 | 18 | describe('getDockerConfig', () => { 19 | it('should return empty config when DOCKER_HOST is set', () => { 20 | process.env.DOCKER_HOST = 'tcp://localhost:2375'; 21 | const config = getDockerConfig(); 22 | expect(config).toEqual({}); 23 | }); 24 | 25 | it('should return custom socket path when provided', () => { 26 | const customPath = '/custom/socket/path'; 27 | const config = getDockerConfig(customPath); 28 | expect(config).toEqual({ socketPath: customPath }); 29 | }); 30 | 31 | it('should detect Docker socket at default location', () => { 32 | // Mock fs.existsSync and fs.statSync 33 | jest.spyOn(fs, 'existsSync').mockImplementation((path) => { 34 | return path === '/var/run/docker.sock'; 35 | }); 36 | jest.spyOn(fs, 'statSync').mockImplementation(() => ({ 37 | isSocket: () => true 38 | })); 39 | 40 | const config = getDockerConfig(); 41 | expect(config).toEqual({ socketPath: '/var/run/docker.sock' }); 42 | 43 | fs.existsSync.mockRestore(); 44 | fs.statSync.mockRestore(); 45 | }); 46 | 47 | it('should detect Podman rootless socket', () => { 48 | const expectedPath = `/run/user/${process.getuid?.() || 1000}/podman/podman.sock`; 49 | 50 | jest.spyOn(fs, 'existsSync').mockImplementation((path) => { 51 | return path === expectedPath; 52 | }); 53 | jest.spyOn(fs, 'statSync').mockImplementation(() => ({ 54 | isSocket: () => true 55 | })); 56 | 57 | const config = getDockerConfig(); 58 | expect(config).toEqual({ socketPath: expectedPath }); 59 | 60 | fs.existsSync.mockRestore(); 61 | fs.statSync.mockRestore(); 62 | }); 63 | 64 | it('should detect Podman root socket', () => { 65 | jest.spyOn(fs, 'existsSync').mockImplementation((path) => { 66 | return path === '/run/podman/podman.sock'; 67 | }); 68 | jest.spyOn(fs, 'statSync').mockImplementation(() => ({ 69 | isSocket: () => true 70 | })); 71 | 72 | const config = getDockerConfig(); 73 | expect(config).toEqual({ socketPath: '/run/podman/podman.sock' }); 74 | 75 | fs.existsSync.mockRestore(); 76 | fs.statSync.mockRestore(); 77 | }); 78 | 79 | it('should use XDG_RUNTIME_DIR for Podman socket if available', () => { 80 | process.env.XDG_RUNTIME_DIR = '/run/user/1000'; 81 | const expectedPath = '/run/user/1000/podman/podman.sock'; 82 | 83 | jest.spyOn(fs, 'existsSync').mockImplementation((path) => { 84 | return path === expectedPath; 85 | }); 86 | jest.spyOn(fs, 'statSync').mockImplementation(() => ({ 87 | isSocket: () => true 88 | })); 89 | 90 | const config = getDockerConfig(); 91 | expect(config).toEqual({ socketPath: expectedPath }); 92 | 93 | fs.existsSync.mockRestore(); 94 | fs.statSync.mockRestore(); 95 | }); 96 | 97 | it('should return empty config when no socket is found', () => { 98 | jest.spyOn(fs, 'existsSync').mockReturnValue(false); 99 | 100 | const config = getDockerConfig(); 101 | expect(config).toEqual({}); 102 | 103 | fs.existsSync.mockRestore(); 104 | }); 105 | 106 | it('should handle file system errors gracefully', () => { 107 | jest.spyOn(fs, 'existsSync').mockImplementation(() => { 108 | throw new Error('Permission denied'); 109 | }); 110 | 111 | const config = getDockerConfig(); 112 | expect(config).toEqual({}); 113 | 114 | fs.existsSync.mockRestore(); 115 | }); 116 | }); 117 | 118 | describe('isPodman', () => { 119 | it('should return true for Podman socket paths', () => { 120 | expect(isPodman({ socketPath: '/run/podman/podman.sock' })).toBe(true); 121 | expect(isPodman({ socketPath: '/run/user/1000/podman/podman.sock' })).toBe(true); 122 | expect(isPodman({ socketPath: '/var/lib/podman/podman.sock' })).toBe(true); 123 | }); 124 | 125 | it('should return false for Docker socket paths', () => { 126 | expect(isPodman({ socketPath: '/var/run/docker.sock' })).toBe(false); 127 | expect(isPodman({ socketPath: '/custom/docker.sock' })).toBe(false); 128 | }); 129 | 130 | it('should return false when no socket path is provided', () => { 131 | expect(isPodman({})).toBe(false); 132 | expect(isPodman({ socketPath: undefined })).toBe(false); 133 | }); 134 | }); 135 | 136 | describe('Integration with configuration', () => { 137 | it('should properly integrate with SandboxConfig', () => { 138 | const sandboxConfig = { 139 | dockerSocketPath: '/custom/podman/socket' 140 | }; 141 | 142 | const dockerConfig = getDockerConfig(sandboxConfig.dockerSocketPath); 143 | expect(dockerConfig).toEqual({ socketPath: '/custom/podman/socket' }); 144 | expect(isPodman(dockerConfig)).toBe(true); 145 | }); 146 | }); 147 | }); -------------------------------------------------------------------------------- /docs/claude-code-docs/Claude Code overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Claude Code overview" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/overview" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Learn about Claude Code, an agentic coding tool made by Anthropic." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster through natural language commands. By integrating directly with your development environment, Claude Code streamlines your workflow without requiring additional servers or complex setup. 14 | 15 | Claude Code’s key capabilities include: 16 | 17 | - Editing files and fixing bugs across your codebase 18 | - Answering questions about your code’s architecture and logic 19 | - Executing and fixing tests, linting, and other commands 20 | - Searching through git history, resolving merge conflicts, and creating commits and PRs 21 | - Browsing documentation and resources from the internet using web search 22 | - Works with [Amazon Bedrock and Google Vertex AI](https://docs.anthropic.com/en/docs/claude-code/bedrock-vertex-proxies) for enterprise deployments 23 | 24 | ## Why Claude Code? 25 | 26 | Claude Code operates directly in your terminal, understanding your project context and taking real actions. No need to manually add files to context - Claude will explore your codebase as needed. 27 | 28 | ### Enterprise integration 29 | 30 | Claude Code seamlessly integrates with enterprise AI platforms. You can connect to [Amazon Bedrock or Google Vertex AI](https://docs.anthropic.com/en/docs/claude-code/bedrock-vertex-proxies) for secure, compliant deployments that meet your organization’s requirements. 31 | 32 | ### Security and privacy by design 33 | 34 | Your code’s security is paramount. Claude Code’s architecture ensures: 35 | 36 | - **Direct API connection**: Your queries go straight to Anthropic’s API without intermediate servers 37 | - **Works where you work**: Operates directly in your terminal 38 | - **Understands context**: Maintains awareness of your entire project structure 39 | - **Takes action**: Performs real operations like editing files and creating commits 40 | 41 | ## Getting started 42 | 43 | To get started with Claude Code, follow our [installation guide](https://docs.anthropic.com/en/docs/claude-code/getting-started) which covers system requirements, installation steps, and authentication process. 44 | 45 | ## Quick tour 46 | 47 | Here’s what you can accomplish with Claude Code: 48 | 49 | ### From questions to solutions in seconds 50 | 51 | ### Understand unfamiliar code 52 | 53 | ### Automate Git operations## [Getting started](https://docs.anthropic.com/en/docs/claude-code/getting-started) 54 | 55 | [ 56 | 57 | Install Claude Code and get up and running 58 | 59 | ](https://docs.anthropic.com/en/docs/claude-code/getting-started)Core features 60 | 61 | Explore what Claude Code can do for you 62 | 63 | [View original](https://docs.anthropic.com/en/docs/claude-code/common-tasks)Commands 64 | 65 | Learn about CLI commands and controls 66 | 67 | [View original](https://docs.anthropic.com/en/docs/claude-code/cli-usage)Configuration 68 | 69 | Customize Claude Code for your workflow 70 | 71 | [View original](https://docs.anthropic.com/en/docs/claude-code/settings) 72 | 73 | ## Additional resources## [Claude Code tutorials](https://docs.anthropic.com/en/docs/claude-code/tutorials) 74 | 75 | [ 76 | 77 | Step-by-step guides for common tasks 78 | 79 | ](https://docs.anthropic.com/en/docs/claude-code/tutorials)Troubleshooting 80 | 81 | Solutions for common issues with Claude Code 82 | 83 | [View original](https://docs.anthropic.com/en/docs/claude-code/troubleshooting)Bedrock & Vertex integrations 84 | 85 | Configure Claude Code with Amazon Bedrock or Google Vertex AI 86 | 87 | [View original](https://docs.anthropic.com/en/docs/claude-code/bedrock-vertex-proxies)Reference implementation 88 | 89 | Clone our development container reference implementation. 90 | 91 | [View original](https://github.com/anthropics/claude-code/tree/main/.devcontainer) 92 | 93 | Claude Code is provided under Anthropic’s [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms). 94 | 95 | ### How we use your data 96 | 97 | We aim to be fully transparent about how we use your data. We may use feedback to improve our products and services, but we will not train generative models using your feedback from Claude Code. Given their potentially sensitive nature, we store user feedback transcripts for only 30 days. 98 | 99 | If you choose to send us feedback about Claude Code, such as transcripts of your usage, Anthropic may use that feedback to debug related issues and improve Claude Code’s functionality (e.g., to reduce the risk of similar bugs occurring in the future). We will not train generative models using this feedback. 100 | 101 | ### Privacy safeguards 102 | 103 | We have implemented several safeguards to protect your data, including limited retention periods for sensitive information, restricted access to user session data, and clear policies against using feedback for model training. 104 | 105 | For full details, please review our [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms) and [Privacy Policy](https://www.anthropic.com/legal/privacy). 106 | 107 | ### License 108 | 109 | © Anthropic PBC. All rights reserved. Use is subject to Anthropic’s [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms). 110 | -------------------------------------------------------------------------------- /docs/claude-code-docs/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Troubleshooting" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/troubleshooting" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-26 8 | description: "Solutions for common issues with Claude Code installation and usage." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | ## Common installation issues 14 | 15 | When installing Claude Code with npm, you may encounter permission errors if your npm global prefix is not user writable (eg. `/usr`, or `/use/local`). 16 | 17 | The safest approach is to configure npm to use a directory within your home folder: 18 | 19 | ```bash 20 | # First, save a list of your existing global packages for later migration 21 | npm list -g --depth=0 > ~/npm-global-packages.txt 22 | 23 | # Create a directory for your global packages 24 | mkdir -p ~/.npm-global 25 | 26 | # Configure npm to use the new directory path 27 | npm config set prefix ~/.npm-global 28 | 29 | # Note: Replace ~/.bashrc with ~/.zshrc, ~/.profile, or other appropriate file for your shell 30 | echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc 31 | 32 | # Apply the new PATH setting 33 | source ~/.bashrc 34 | 35 | # Now reinstall Claude Code in the new location 36 | npm install -g @anthropic-ai/claude-code 37 | 38 | # Optional: Reinstall your previous global packages in the new location 39 | # Look at ~/npm-global-packages.txt and install packages you want to keep 40 | ``` 41 | 42 | This solution is recommended because it: 43 | 44 | - Avoids modifying system directory permissions 45 | - Creates a clean, dedicated location for your global npm packages 46 | - Follows security best practices 47 | 48 | #### System Recovery: If you have run commands that change ownership and permissions of system files or similar 49 | 50 | If you’ve already run a command that changed system directory permissions (such as `sudo chown -R $USER:$(id -gn) /usr && sudo chmod -R u+w /usr`) and your system is now broken (for example, if you see `sudo: /usr/bin/sudo must be owned by uid 0 and have the setuid bit set`), you’ll need to perform recovery steps. 51 | 52 | ##### Ubuntu/Debian Recovery Method: 53 | 54 | 1. While rebooting, hold **SHIFT** to access the GRUB menu 55 | 2. Select “Advanced options for Ubuntu/Debian” 56 | 3. Choose the recovery mode option 57 | 4. Select “Drop to root shell prompt” 58 | 5. Remount the filesystem as writable: 59 | 6. Fix permissions: 60 | 7. Reinstall affected packages (optional but recommended): 61 | 8. Reboot: 62 | ```bash 63 | reboot 64 | ``` 65 | 66 | ##### Alternative Live USB Recovery Method: 67 | 68 | If the recovery mode doesn’t work, you can use a live USB: 69 | 70 | 1. Boot from a live USB (Ubuntu, Debian, or any Linux distribution) 71 | 2. Find your system partition: 72 | ```bash 73 | lsblk 74 | ``` 75 | 3. Mount your system partition: 76 | 4. If you have a separate boot partition, mount it too: 77 | 5. Chroot into your system: 78 | 6. Follow steps 6-8 from the Ubuntu/Debian recovery method above 79 | 80 | After restoring your system, follow the recommended solution above to set up a user-writable npm prefix. 81 | 82 | ## Auto-updater issues 83 | 84 | If Claude Code can’t update automatically, it may be due to permission issues with your npm global prefix directory. Follow the [recommended solution](https://docs.anthropic.com/en/docs/claude-code/troubleshooting#recommended-solution-create-a-user-writable-npm-prefix) above to fix this. 85 | 86 | If you prefer to disable the auto-updater instead, you can use: 87 | 88 | ## Permissions and authentication 89 | 90 | If you find yourself repeatedly approving the same commands, you can allow specific tools to run without approval: 91 | 92 | ### Authentication issues 93 | 94 | If you’re experiencing authentication problems: 95 | 96 | 1. Run `/logout` to sign out completely 97 | 2. Close Claude Code 98 | 3. Restart with `claude` and complete the authentication process again 99 | 100 | If problems persist, try: 101 | 102 | This removes your stored authentication information and forces a clean login. 103 | 104 | ## Performance and stability 105 | 106 | ### High CPU or memory usage 107 | 108 | Claude Code is designed to work with most development environments, but may consume significant resources when processing large codebases. If you’re experiencing performance issues: 109 | 110 | 1. Use `/compact` regularly to reduce context size 111 | 2. Close and restart Claude Code between major tasks 112 | 3. Consider adding large build directories to your `.gitignore` file 113 | 114 | ### Command hangs or freezes 115 | 116 | If Claude Code seems unresponsive: 117 | 118 | 1. Press Ctrl+C to attempt to cancel the current operation 119 | 2. If unresponsive, you may need to close the terminal and restart 120 | 121 | ### ESC key not working in JetBrains (IntelliJ, PyCharm, etc.) terminals 122 | 123 | If you’re using Claude Code in JetBrains terminals and the ESC key doesn’t interrupt the agent as expected, this is likely due to a keybinding clash with JetBrains’ default shortcuts. 124 | 125 | To fix this issue: 126 | 127 | 1. Go to Settings → Tools → Terminal 128 | 2. Click the “Configure terminal keybindings” hyperlink next to “Override IDE Shortcuts” 129 | 3. Within the terminal keybindings, scroll down to “Switch focus to Editor” and delete that shortcut 130 | 131 | This will allow the ESC key to properly function for canceling Claude Code operations instead of being captured by PyCharm’s “Switch focus to Editor” action. 132 | 133 | If you’re experiencing issues not covered here: 134 | 135 | 1. Use the `/bug` command within Claude Code to report problems directly to Anthropic 136 | 2. Check the [GitHub repository](https://github.com/anthropics/claude-code) for known issues 137 | 3. Run `/doctor` to check the health of your Claude Code installation 138 | -------------------------------------------------------------------------------- /docs/environment-variables.md: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | 3 | This document explains how to pass environment variables to Claude Sandbox containers. 4 | 5 | ## Overview 6 | 7 | Claude Sandbox supports two ways to pass environment variables to containers: 8 | 9 | 1. **Inline environment variables** in the configuration file 10 | 2. **Loading from a `.env` file** 11 | 12 | Both methods can be used together, with inline variables taking precedence over those loaded from a file. 13 | 14 | ## Configuration 15 | 16 | ### Inline Environment Variables 17 | 18 | Add environment variables directly in your `claude-sandbox.config.json`: 19 | 20 | ```json 21 | { 22 | "environment": { 23 | "API_KEY": "your-api-key", 24 | "DATABASE_URL": "postgresql://user:pass@host:5432/db", 25 | "NODE_ENV": "development", 26 | "DEBUG": "true" 27 | } 28 | } 29 | ``` 30 | 31 | ### Loading from .env File 32 | 33 | Specify a path to a `.env` file to load: 34 | 35 | ```json 36 | { 37 | "envFile": ".env", 38 | "environment": { 39 | "OVERRIDE_VAR": "this-overrides-env-file" 40 | } 41 | } 42 | ``` 43 | 44 | The `.env` file format: 45 | 46 | ```bash 47 | # Comments are supported 48 | API_KEY=your-api-key 49 | DATABASE_URL=postgresql://user:pass@host:5432/db 50 | 51 | # Empty lines are ignored 52 | 53 | # Quotes are optional but removed if present 54 | QUOTED_VAR="value with spaces" 55 | SINGLE_QUOTED='another value' 56 | 57 | # Values can contain = signs 58 | CONNECTION_STRING=key=value;another=value 59 | 60 | # Export statements are ignored (just use KEY=VALUE) 61 | NODE_ENV=development 62 | ``` 63 | 64 | ## Precedence Order 65 | 66 | Environment variables are loaded in this order (later sources override earlier ones): 67 | 68 | 1. Variables from `.env` file (if specified) 69 | 2. Inline `environment` configuration 70 | 3. Claude credentials (ANTHROPIC_API_KEY, etc.) 71 | 4. GitHub token (GITHUB_TOKEN) 72 | 5. Git author information (GIT_AUTHOR_NAME, etc.) 73 | 6. System variables (MAX_THINKING_TOKENS, etc.) 74 | 75 | ## Examples 76 | 77 | ### Basic Configuration 78 | 79 | ```json 80 | { 81 | "dockerImage": "claude-code-sandbox:latest", 82 | "environment": { 83 | "MY_APP_KEY": "12345", 84 | "API_ENDPOINT": "https://api.example.com" 85 | } 86 | } 87 | ``` 88 | 89 | ### Using .env File 90 | 91 | Create `.env`: 92 | 93 | ```bash 94 | # Development settings 95 | DATABASE_URL=postgresql://localhost:5432/myapp 96 | REDIS_URL=redis://localhost:6379 97 | SECRET_KEY=development-secret 98 | DEBUG=true 99 | ``` 100 | 101 | Configure `claude-sandbox.config.json`: 102 | 103 | ```json 104 | { 105 | "envFile": ".env", 106 | "environment": { 107 | "NODE_ENV": "development" 108 | } 109 | } 110 | ``` 111 | 112 | ### Multiple Environment Files 113 | 114 | For different environments, use different config files: 115 | 116 | `claude-sandbox.dev.json`: 117 | 118 | ```json 119 | { 120 | "envFile": ".env.development", 121 | "environment": { 122 | "NODE_ENV": "development" 123 | } 124 | } 125 | ``` 126 | 127 | `claude-sandbox.prod.json`: 128 | 129 | ```json 130 | { 131 | "envFile": ".env.production", 132 | "environment": { 133 | "NODE_ENV": "production" 134 | } 135 | } 136 | ``` 137 | 138 | Run with: 139 | 140 | ```bash 141 | claude-sandbox --config claude-sandbox.dev.json 142 | ``` 143 | 144 | ## Security Best Practices 145 | 146 | 1. **Never commit sensitive data**: Add `.env` files to `.gitignore` 147 | 148 | ```gitignore 149 | .env 150 | .env.* 151 | claude-sandbox.config.json 152 | ``` 153 | 154 | 2. **Use placeholder values** in committed config files: 155 | 156 | ```json 157 | { 158 | "environment": { 159 | "API_KEY": "REPLACE_ME" 160 | } 161 | } 162 | ``` 163 | 164 | 3. **Use .env files** for sensitive data: 165 | 166 | - Keep `.env` files local 167 | - Use `.env.example` with dummy values for documentation 168 | 169 | 4. **Validate required variables** in setup commands: 170 | ```json 171 | { 172 | "setupCommands": [ 173 | "test -n \"$API_KEY\" || (echo 'Error: API_KEY not set' && exit 1)" 174 | ] 175 | } 176 | ``` 177 | 178 | ## Special Environment Variables 179 | 180 | These variables have special meaning in Claude Sandbox: 181 | 182 | ### Claude Configuration 183 | 184 | - `ANTHROPIC_API_KEY` - Claude API key 185 | - `CLAUDE_CODE_USE_BEDROCK` - Use AWS Bedrock 186 | - `CLAUDE_CODE_USE_VERTEX` - Use Google Vertex 187 | - `MAX_THINKING_TOKENS` - Maximum thinking tokens 188 | - `BASH_MAX_TIMEOUT_MS` - Bash command timeout 189 | 190 | ### GitHub Configuration 191 | 192 | - `GITHUB_TOKEN` - GitHub authentication token 193 | - `GH_TOKEN` - Alternative GitHub token variable 194 | - `GIT_AUTHOR_NAME` - Git commit author name 195 | - `GIT_AUTHOR_EMAIL` - Git commit author email 196 | 197 | ### System Configuration 198 | 199 | - `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` - Always set to 1 200 | 201 | ## Debugging 202 | 203 | To see what environment variables are available in the container: 204 | 205 | ```bash 206 | # In the container 207 | env | sort 208 | 209 | # Or check specific variables 210 | echo $MY_VAR 211 | ``` 212 | 213 | ## Common Use Cases 214 | 215 | ### API Keys and Secrets 216 | 217 | ```json 218 | { 219 | "envFile": ".env.secrets", 220 | "environment": { 221 | "API_VERSION": "v1" 222 | } 223 | } 224 | ``` 225 | 226 | ### Database Configuration 227 | 228 | ```json 229 | { 230 | "environment": { 231 | "DB_HOST": "localhost", 232 | "DB_PORT": "5432", 233 | "DB_NAME": "myapp" 234 | }, 235 | "envFile": ".env.local" 236 | } 237 | ``` 238 | 239 | ### Feature Flags 240 | 241 | ```json 242 | { 243 | "environment": { 244 | "FEATURE_NEW_UI": "true", 245 | "FEATURE_BETA_API": "false" 246 | } 247 | } 248 | ``` 249 | 250 | ### Development Tools 251 | 252 | ```json 253 | { 254 | "environment": { 255 | "DEBUG": "*", 256 | "LOG_LEVEL": "verbose", 257 | "PRETTY_PRINT": "true" 258 | } 259 | } 260 | ``` 261 | -------------------------------------------------------------------------------- /test/e2e/README.md: -------------------------------------------------------------------------------- 1 | # Claude Sandbox E2E Tests 2 | 3 | This directory contains end-to-end tests for the Claude Sandbox file synchronization functionality. These tests verify that file operations (create, modify, delete) are properly synced between the container and the shadow repository, and that the web UI receives appropriate notifications. 4 | 5 | ## Overview 6 | 7 | The tests create a temporary git repository, start a claude-sandbox instance, perform file operations inside the container, and verify that: 8 | 9 | 1. Files are properly synced to the shadow repository 10 | 2. Git properly tracks additions, modifications, and deletions 11 | 3. The web UI receives sync notifications 12 | 4. File content is accurately preserved 13 | 14 | ## Test Structure 15 | 16 | ### Core Components 17 | 18 | - **`sync-test-framework.js`** - Main testing framework that manages sandbox lifecycle 19 | - **`dummy-repo/`** - Template files for creating test repositories 20 | - **`repo-to-container-sync-test.js`** - Verifies one-to-one sync from repo to container 21 | - **`core-functionality-test.js`** - Essential functionality tests (recommended) 22 | - **`simple-deletion-test.js`** - Focused test for deletion tracking 23 | - **`test-suite.js`** - Comprehensive test suite with all scenarios 24 | - **`run-tests.sh`** - Shell script for automated test execution 25 | 26 | ### Test Categories 27 | 28 | 1. **Repository to Container Sync** - Verifying one-to-one sync from local repo to container 29 | 2. **File Addition** - Creating new files and verifying sync 30 | 3. **File Modification** - Modifying existing files and tracking changes 31 | 4. **File Deletion** - Deleting files and ensuring proper removal 32 | 5. **Directory Operations** - Creating nested directories and files 33 | 6. **Web UI Notifications** - Verifying real-time sync events 34 | 35 | ## Running Tests 36 | 37 | ### Quick Test (Recommended) 38 | 39 | ```bash 40 | # Run core functionality tests 41 | node core-functionality-test.js 42 | ``` 43 | 44 | ### Individual Tests 45 | 46 | ```bash 47 | # Test repository to container sync 48 | node repo-to-container-sync-test.js 49 | 50 | # Test deletion functionality specifically 51 | node simple-deletion-test.js 52 | 53 | # Run comprehensive test suite 54 | node test-suite.js 55 | ``` 56 | 57 | ### Automated Test Runner 58 | 59 | ```bash 60 | # Run all tests with cleanup 61 | ./run-tests.sh 62 | ``` 63 | 64 | ## Prerequisites 65 | 66 | - Node.js (for running test scripts) 67 | - Docker (for claude-sandbox containers) 68 | - Built claude-sandbox project (`npm run build`) 69 | 70 | ## Test Process 71 | 72 | 1. **Setup Phase** 73 | 74 | - Creates temporary git repository with dummy files 75 | - Starts claude-sandbox instance 76 | - Connects to web UI for monitoring sync events 77 | 78 | 2. **Test Execution** 79 | 80 | - Performs file operations inside the container 81 | - Waits for synchronization to complete 82 | - Verifies shadow repository state 83 | - Checks git status and file content 84 | 85 | 3. **Cleanup Phase** 86 | - Terminates sandbox processes 87 | - Removes containers and temporary files 88 | 89 | ## Key Features Tested 90 | 91 | ### Repository to Container Sync 92 | 93 | - ✅ One-to-one file mapping from test repo to container 94 | - ✅ No extra files in container (only test repo files) 95 | - ✅ File content integrity verification 96 | - ✅ Git repository properly initialized 97 | - ✅ Correct branch creation 98 | 99 | ### File Synchronization 100 | 101 | - ✅ New file creation and content sync 102 | - ✅ File modification and content updates 103 | - ✅ File deletion and proper removal 104 | - ✅ Directory creation and nested files 105 | - ✅ Large file handling 106 | - ✅ Special characters in filenames 107 | 108 | ### Git Integration 109 | 110 | - ✅ Staging of additions (`A` status) 111 | - ✅ Tracking of modifications (`M` status) 112 | - ✅ Detection of deletions (`D` status) 113 | - ✅ Proper git commit workflow 114 | 115 | ### Web UI Integration 116 | 117 | - ✅ Real-time sync event notifications 118 | - ✅ Change summary reporting 119 | - ✅ WebSocket communication 120 | 121 | ## Troubleshooting 122 | 123 | ### Common Issues 124 | 125 | **Container startup timeout** 126 | 127 | - Increase timeout values in test framework 128 | - Check Docker daemon is running 129 | - Verify claude-sandbox image exists 130 | 131 | **Git lock conflicts** 132 | 133 | - Tests automatically handle concurrent git operations 134 | - Temporary `.git/index.lock` files are cleaned up 135 | 136 | **Port conflicts** 137 | 138 | - Tests use dynamic port allocation 139 | - Multiple tests can run sequentially 140 | 141 | **WebSocket connection issues** 142 | 143 | - Framework includes connection retry logic 144 | - Fallback to polling if WebSocket fails 145 | 146 | ### Test Failure Analysis 147 | 148 | Tests provide detailed error messages indicating: 149 | 150 | - Which specific operation failed 151 | - Expected vs actual file states 152 | - Git status differences 153 | - Sync event discrepancies 154 | 155 | ## Development 156 | 157 | ### Adding New Tests 158 | 159 | 1. Create test function in appropriate test file 160 | 2. Use framework methods for file operations: 161 | ```javascript 162 | await framework.addFile("path/file.txt", "content"); 163 | await framework.modifyFile("path/file.txt", "new content"); 164 | await framework.deleteFile("path/file.txt"); 165 | ``` 166 | 3. Verify results using assertion methods: 167 | ```javascript 168 | const exists = await framework.shadowFileExists("file.txt"); 169 | const content = await framework.getShadowFileContent("file.txt"); 170 | const gitStatus = await framework.getGitStatus(); 171 | ``` 172 | 173 | ### Framework API 174 | 175 | **File Operations** 176 | 177 | - `addFile(path, content)` - Create new file 178 | - `modifyFile(path, content)` - Update existing file 179 | - `deleteFile(path)` - Remove file 180 | - `moveFile(from, to)` - Rename/move file 181 | - `createDirectory(path)` - Create directory 182 | 183 | **Verification Methods** 184 | 185 | - `shadowFileExists(path)` - Check file existence 186 | - `getShadowFileContent(path)` - Read file content 187 | - `getGitStatus()` - Get git status output 188 | - `waitForSync()` - Wait for synchronization 189 | 190 | **Event Monitoring** 191 | 192 | - `receivedSyncEvents` - Array of sync notifications 193 | - WebSocket connection automatically established 194 | 195 | ## Integration with CI/CD 196 | 197 | These tests are designed to run in automated environments: 198 | 199 | ```yaml 200 | # Example GitHub Actions workflow 201 | - name: Run E2E Tests 202 | run: | 203 | npm run build 204 | cd e2e-tests 205 | ./run-tests.sh 206 | ``` 207 | 208 | The tests provide proper exit codes (0 for success, 1 for failure) and detailed logging for debugging purposes. 209 | -------------------------------------------------------------------------------- /test/e2e/core-functionality-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { SyncTestFramework } = require("./sync-test-framework"); 4 | 5 | async function runCoreTests() { 6 | console.log("🚀 Core File Sync Functionality Tests"); 7 | console.log("====================================="); 8 | 9 | const framework = new SyncTestFramework(); 10 | const tests = []; 11 | 12 | try { 13 | await framework.setup(); 14 | 15 | // Test 0: Initial Repository Sync 16 | console.log("\n🧪 Test 0: Initial Repository to Container Sync"); 17 | 18 | // Verify that repository files are properly synced to container 19 | const repoFiles = await framework.listRepoFiles(); 20 | const containerFiles = await framework.listContainerFiles(); 21 | 22 | // Check that key files exist in container 23 | const expectedFiles = [ 24 | "README.md", 25 | "package.json", 26 | "src/main.js", 27 | "src/utils.js", 28 | ]; 29 | for (const file of expectedFiles) { 30 | const exists = await framework.containerFileExists(file); 31 | if (!exists) { 32 | throw new Error(`Expected file ${file} not found in container`); 33 | } 34 | 35 | // Verify content matches 36 | const repoContent = await require("fs").promises.readFile( 37 | require("path").join(framework.testRepo, file), 38 | "utf8", 39 | ); 40 | const containerContent = await framework.getContainerFileContent(file); 41 | 42 | if (repoContent.trim() !== containerContent.trim()) { 43 | throw new Error(`Content mismatch for ${file}`); 44 | } 45 | } 46 | 47 | console.log("✅ Initial repository sync test passed"); 48 | tests.push({ name: "Initial Repository Sync", passed: true }); 49 | 50 | // Test 1: File Addition 51 | console.log("\n🧪 Test 1: File Addition"); 52 | await framework.addFile("addition-test.txt", "New file content"); 53 | 54 | const addExists = await framework.shadowFileExists("addition-test.txt"); 55 | if (!addExists) throw new Error("File addition failed"); 56 | 57 | const addContent = 58 | await framework.getShadowFileContent("addition-test.txt"); 59 | if (addContent.trim() !== "New file content") 60 | throw new Error("File content mismatch"); 61 | 62 | const addStatus = await framework.getGitStatus(); 63 | if (!addStatus.includes("addition-test.txt")) 64 | throw new Error("Git should track new file"); 65 | 66 | console.log("✅ File addition test passed"); 67 | tests.push({ name: "File Addition", passed: true }); 68 | 69 | // Test 2: File Modification 70 | console.log("\n🧪 Test 2: File Modification"); 71 | // Commit first so we can see modifications 72 | await require("util").promisify(require("child_process").exec)( 73 | `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add files for testing"`, 74 | ); 75 | 76 | await framework.modifyFile("addition-test.txt", "Modified content"); 77 | 78 | const modContent = 79 | await framework.getShadowFileContent("addition-test.txt"); 80 | if (modContent.trim() !== "Modified content") 81 | throw new Error("File modification failed"); 82 | 83 | const modStatus = await framework.getGitStatus(); 84 | if (!modStatus.includes("M addition-test.txt")) 85 | throw new Error("Git should track modification"); 86 | 87 | console.log("✅ File modification test passed"); 88 | tests.push({ name: "File Modification", passed: true }); 89 | 90 | // Test 3: File Deletion 91 | console.log("\n🧪 Test 3: File Deletion"); 92 | await framework.deleteFile("addition-test.txt"); 93 | 94 | const delExists = await framework.shadowFileExists("addition-test.txt"); 95 | if (delExists) throw new Error("File deletion failed - file still exists"); 96 | 97 | const delStatus = await framework.getGitStatus(); 98 | if (!delStatus.includes("D addition-test.txt")) 99 | throw new Error("Git should track deletion"); 100 | 101 | console.log("✅ File deletion test passed"); 102 | tests.push({ name: "File Deletion", passed: true }); 103 | 104 | // Test 4: Directory Operations 105 | console.log("\n🧪 Test 4: Directory Operations"); 106 | await framework.createDirectory("test-dir"); 107 | await framework.addFile("test-dir/nested-file.txt", "Nested content"); 108 | 109 | const nestedExists = await framework.shadowFileExists( 110 | "test-dir/nested-file.txt", 111 | ); 112 | if (!nestedExists) throw new Error("Nested file creation failed"); 113 | 114 | const nestedContent = await framework.getShadowFileContent( 115 | "test-dir/nested-file.txt", 116 | ); 117 | if (nestedContent.trim() !== "Nested content") 118 | throw new Error("Nested file content mismatch"); 119 | 120 | console.log("✅ Directory operations test passed"); 121 | tests.push({ name: "Directory Operations", passed: true }); 122 | 123 | // Test 5: Web UI Notifications 124 | console.log("\n🧪 Test 5: Web UI Notifications"); 125 | const initialEventCount = framework.receivedSyncEvents.length; 126 | 127 | await framework.addFile("notification-test.txt", "Notification content"); 128 | await framework.waitForSync(); 129 | 130 | const finalEventCount = framework.receivedSyncEvents.length; 131 | if (finalEventCount <= initialEventCount) 132 | throw new Error("No sync events received"); 133 | 134 | const latestEvent = 135 | framework.receivedSyncEvents[framework.receivedSyncEvents.length - 1]; 136 | if (!latestEvent.data.hasChanges) 137 | throw new Error("Sync event should indicate changes"); 138 | 139 | console.log("✅ Web UI notifications test passed"); 140 | tests.push({ name: "Web UI Notifications", passed: true }); 141 | } catch (error) { 142 | console.log(`❌ Test failed: ${error.message}`); 143 | tests.push({ name: "Current Test", passed: false, error: error.message }); 144 | } finally { 145 | await framework.cleanup(); 146 | } 147 | 148 | // Results 149 | console.log("\n" + "=".repeat(50)); 150 | console.log("📊 Test Results:"); 151 | 152 | const passed = tests.filter((t) => t.passed).length; 153 | const failed = tests.filter((t) => !t.passed).length; 154 | 155 | tests.forEach((test) => { 156 | const icon = test.passed ? "✅" : "❌"; 157 | console.log(`${icon} ${test.name}`); 158 | if (!test.passed && test.error) { 159 | console.log(` ${test.error}`); 160 | } 161 | }); 162 | 163 | console.log(`\n📈 Summary: ${passed} passed, ${failed} failed`); 164 | 165 | if (failed === 0) { 166 | console.log("🎉 All core functionality tests passed!"); 167 | return true; 168 | } else { 169 | console.log("❌ Some tests failed"); 170 | return false; 171 | } 172 | } 173 | 174 | runCoreTests() 175 | .then((success) => { 176 | process.exit(success ? 0 : 1); 177 | }) 178 | .catch((error) => { 179 | console.error("❌ Test runner failed:", error); 180 | process.exit(1); 181 | }); 182 | -------------------------------------------------------------------------------- /docs/claude-code-docs/Manage permissions and security.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Manage permissions and security" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/security" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Learn about Claude Code's permission system, tools access, and security safeguards." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | Claude Code uses a tiered permission system to balance power and safety: 14 | 15 | | Tool Type | Example | Approval Required | ”Yes, don’t ask again” Behavior | 16 | | ----------------- | -------------------- | ----------------- | --------------------------------------------- | 17 | | Read-only | File reads, LS, Grep | No | N/A | 18 | | Bash Commands | Shell execution | Yes | Permanently per project directory and command | 19 | | File Modification | Edit/write files | Yes | Until session end | 20 | 21 | ## Tools available to Claude 22 | 23 | Claude Code has access to a set of powerful tools that help it understand and modify your codebase: 24 | 25 | | Tool | Description | Permission Required | 26 | | ---------------- | ---------------------------------------------------- | ------------------- | 27 | | **Agent** | Runs a sub-agent to handle complex, multi-step tasks | No | 28 | | **Bash** | Executes shell commands in your environment | Yes | 29 | | **Glob** | Finds files based on pattern matching | No | 30 | | **Grep** | Searches for patterns in file contents | No | 31 | | **LS** | Lists files and directories | No | 32 | | **Read** | Reads the contents of files | No | 33 | | **Edit** | Makes targeted edits to specific files | Yes | 34 | | **Write** | Creates or overwrites files | Yes | 35 | | **NotebookEdit** | Modifies Jupyter notebook cells | Yes | 36 | | **NotebookRead** | Reads and displays Jupyter notebook contents | No | 37 | | **WebFetch** | Fetches content from a specified URL | Yes | 38 | 39 | Permission rules can be configured using `/allowed-tools` or in [permission settings](https://docs.anthropic.com/en/docs/claude-code/settings#permissions). 40 | 41 | ## Protect against prompt injection 42 | 43 | Prompt injection is a technique where an attacker attempts to override or manipulate an AI assistant’s instructions by inserting malicious text. Claude Code includes several safeguards against these attacks: 44 | 45 | - **Permission system**: Sensitive operations require explicit approval 46 | - **Context-aware analysis**: Detects potentially harmful instructions by analyzing the full request 47 | - **Input sanitization**: Prevents command injection by processing user inputs 48 | - **Command blocklist**: Blocks risky commands that fetch arbitrary content from the web like `curl` and `wget` 49 | 50 | **Best practices for working with untrusted content**: 51 | 52 | 1. Review suggested commands before approval 53 | 2. Avoid piping untrusted content directly to Claude 54 | 3. Verify proposed changes to critical files 55 | 4. Report suspicious behavior with `/bug` 56 | 57 | While these protections significantly reduce risk, no system is completely immune to all attacks. Always maintain good security practices when working with any AI tool. 58 | 59 | ## Configure network access 60 | 61 | Claude Code requires access to: 62 | 63 | - api.anthropic.com 64 | - statsig.anthropic.com 65 | - sentry.io 66 | 67 | Allowlist these URLs when using Claude Code in containerized environments. 68 | 69 | ## Development container reference implementation 70 | 71 | Claude Code provides a development container configuration for teams that need consistent, secure environments. This preconfigured [devcontainer setup](https://code.visualstudio.com/docs/devcontainers/containers) works seamlessly with VS Code’s Remote - Containers extension and similar tools. 72 | 73 | The container’s enhanced security measures (isolation and firewall rules) allow you to run `claude --dangerously-skip-permissions` to bypass permission prompts for unattended operation. We’ve included a [reference implementation](https://github.com/anthropics/claude-code/tree/main/.devcontainer) that you can customize for your needs. 74 | 75 | While the devcontainer provides substantial protections, no system is completely immune to all attacks. Always maintain good security practices and monitor Claude’s activities. 76 | 77 | ### Key features 78 | 79 | - **Production-ready Node.js**: Built on Node.js 20 with essential development dependencies 80 | - **Security by design**: Custom firewall restricting network access to only necessary services 81 | - **Developer-friendly tools**: Includes git, ZSH with productivity enhancements, fzf, and more 82 | - **Seamless VS Code integration**: Pre-configured extensions and optimized settings 83 | - **Session persistence**: Preserves command history and configurations between container restarts 84 | - **Works everywhere**: Compatible with macOS, Windows, and Linux development environments 85 | 86 | ### Getting started in 4 steps 87 | 88 | 1. Install VS Code and the Remote - Containers extension 89 | 2. Clone the [Claude Code reference implementation](https://github.com/anthropics/claude-code/tree/main/.devcontainer) repository 90 | 3. Open the repository in VS Code 91 | 4. When prompted, click “Reopen in Container” (or use Command Palette: Cmd+Shift+P → “Remote-Containers: Reopen in Container”) 92 | 93 | ### Configuration breakdown 94 | 95 | The devcontainer setup consists of three primary components: 96 | 97 | - [**devcontainer.json**](https://github.com/anthropics/claude-code/blob/main/.devcontainer/devcontainer.json): Controls container settings, extensions, and volume mounts 98 | - [**Dockerfile**](https://github.com/anthropics/claude-code/blob/main/.devcontainer/Dockerfile): Defines the container image and installed tools 99 | - [**init-firewall.sh**](https://github.com/anthropics/claude-code/blob/main/.devcontainer/init-firewall.sh): Establishes network security rules 100 | 101 | ### Security features 102 | 103 | The container implements a multi-layered security approach with its firewall configuration: 104 | 105 | - **Precise access control**: Restricts outbound connections to whitelisted domains only (npm registry, GitHub, Anthropic API, etc.) 106 | - **Default-deny policy**: Blocks all other external network access 107 | - **Startup verification**: Validates firewall rules when the container initializes 108 | - **Isolation**: Creates a secure development environment separated from your main system 109 | 110 | ### Customization options 111 | 112 | The devcontainer configuration is designed to be adaptable to your needs: 113 | 114 | - Add or remove VS Code extensions based on your workflow 115 | - Modify resource allocations for different hardware environments 116 | - Adjust network access permissions 117 | - Customize shell configurations and developer tooling 118 | -------------------------------------------------------------------------------- /docs/2025-05-25-first-design.md: -------------------------------------------------------------------------------- 1 | ## Claude Sandbox Requirements Summary 2 | 3 | ### Core Concept 4 | 5 | Claude-sandbox is a tool that runs Claude Code as an interactive agent inside Docker containers, providing a sandboxed environment for autonomous coding with git integration. 6 | 7 | ### Key Functional Requirements 8 | 9 | #### 1. **Launch Behavior** 10 | 11 | - Simple command: just type `claude-sandbox` in any git repository 12 | - No pre-specified task - users interact with Claude directly after launch 13 | - Claude Code runs as an interactive REPL (like a smart terminal) 14 | - Maintains persistent context throughout the session (never exits) 15 | 16 | #### 2. **Sandboxing** 17 | 18 | - Runs inside Docker container for complete isolation 19 | - Claude has full permissions to run any command (since it's sandboxed) 20 | - No permission prompts - everything is auto-allowed 21 | - Environment can be customized via Dockerfile configuration 22 | 23 | #### 3. **Credential Management** 24 | 25 | - Automatically discovers and forwards Claude credentials from host: 26 | - Claude Max OAuth tokens 27 | - Anthropic API keys 28 | - AWS Bedrock credentials 29 | - Automatically forwards GitHub credentials: 30 | - GitHub CLI authentication 31 | - SSH keys 32 | - Git configuration 33 | 34 | #### 4. **Git Workflow** 35 | 36 | - **CRITICAL**: NO branch switching happens on the host machine - EVER 37 | - Host repository stays on whatever branch the user is currently on 38 | - **CRITICAL**: Files must be COPIED into the container, NOT mounted 39 | - This ensures git operations in the container don't affect the host 40 | - Container gets a snapshot of the current working directory 41 | - Changes made in container don't affect host until explicitly exported 42 | - Inside the container: 43 | - A new branch `claude/[timestamp]` is created from the current state 44 | - All work happens on this new branch 45 | - Claude can make commits but cannot switch to other branches 46 | - Git wrapper prevents branch switching operations within container 47 | 48 | #### 5. **Change Detection & Review** 49 | 50 | - Real-time monitoring for new commits 51 | - When commit detected: 52 | - User gets notification 53 | - Can detach from Claude session 54 | - Review full diffs with syntax highlighting 55 | - See commit statistics 56 | - Interactive review options: 57 | - Do nothing (continue working) 58 | - Push branch to remote 59 | - Push branch and create PR 60 | - Exit 61 | 62 | #### 6. **Asynchronous Operation** 63 | 64 | - Multiple containers can run simultaneously 65 | - Each gets its own branch and isolated environment 66 | - Fire-and-forget model for parallel work 67 | 68 | ### Technical Implementation Details 69 | 70 | #### Docker Container Setup 71 | 72 | - Base image with all necessary tools (git, gh CLI, build tools) 73 | - Claude Code installed globally 74 | - SSH configured for GitHub 75 | - Auto-permission wrapper to bypass all prompts 76 | 77 | #### Monitoring System 78 | 79 | - Watches for file changes 80 | - Detects git commits 81 | - Tracks Claude's activity state 82 | - Determines when tasks are complete 83 | 84 | #### User Experience Flow 85 | 86 | 1. Run `claude-sandbox` in repo 87 | 2. System creates new branch 88 | 3. Docker container starts with Claude Code 89 | 4. User chats with Claude naturally 90 | 5. Claude works, makes changes, commits 91 | 6. User sees commit notifications 92 | 7. Can detach anytime to review changes 93 | 8. Choose to push/PR after review 94 | 95 | ### Design Principles 96 | 97 | - **Zero configuration** - works out of the box 98 | - **Natural interaction** - chat with Claude like normal 99 | - **Full visibility** - see all changes before they leave the machine 100 | - **Safe by default** - everything happens in isolated branches 101 | - **Credential security** - read-only mounts, no credential exposure 102 | 103 | This creates an experience where Claude Code becomes a powerful local development companion that can work autonomously while maintaining full user control over what gets pushed to the repository. 104 | 105 | --- 106 | 107 | ## Original Requirements 108 | 109 | Look into Claude Code documentation. I want to be able to run it locally as an asynchronous agent inside Docker containers. So basically I will go into a repo, and then I just want to be able to fire off a command line tool. This command will automatically start a Docker container and fire off Claude code. The thing with Cohort code is it's the terminal utility/agent that can run commands, and the point of firing it in a Docker container is to provide a sandbox to it. The sandbox should automatically notice when Claude Code makes a change to the code. First, it should detect if Claude Code is finished with his job. And when it stops responding, and it detects that Claude code has made some line changes. It should push those changes to a new branch in the GitHub repo which it was started in. Since Claude code will be running inside a sandbox, it should be started with the mode which lets it run any command at once because it won't have access to the outside thing. So it should also be able to change the code however it likes or run any command that is available in the environment. Because it will stay inside the container and will not reach outside the container. The tool should furthermore have configuration like where the user is able to specify the environment setup possibly through a Dockerfile configuration or something like that. The basic function of this utility is to let you run fire off agents locally and work on them asynchronously, like OpenAI Codex or Google Jules. First of all, do some research on Claude Code, the most recent publishing and documentations on Anthropic website. Then, clean up this writing, make it concise and comprehensive, and outline how this thing could be implemented. You get the idea. If there are any missing points, fill in the gaps. 110 | 111 | One thing, Claude shouldn't exit. If it exits, then the context is lost. There should be a way to figure out that it's done without making it quit. Also, this tool should automatically get the credential from the "outside" claude code is using (whether that is local bearer tokens to an account with claude max plan or an anthropic api key) and use it in the container. Also, it should be able to run all the tools/commands without permission, since this is a sandbox. 112 | 113 | It should also get the outside github credential, to be able to push a new branch and create a new PR. In fact, the container should start with a new branch. Then, Claude Code should be able to make a commit (but not switch to a different branch. It has to stay in the current branch). As soon as a commit is made, the tool should detect it. The tool should then let the user review the diffs, line of diffs of the code. After reviewing, it should let the user do a) do nothing, b) push the branch, or c) push the branch and create a PR. 114 | 115 | This tool should be called claude-sandbox. You should just be able to fire it without running a command. Claude code is like a very smart repl that just does stuff for you. So most people would directly type the command after claude starts, not in the terminal. Make sure your implementation matches the most recent claude code interface/api, allowed tools, etc. Look at the most recent documentation. Claude code github actions can be your reference: https://github.com/anthropics/claude-code-base-action https://github.com/anthropics/claude-code-action 116 | -------------------------------------------------------------------------------- /test/e2e/repo-to-container-sync-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { SyncTestFramework } = require("./sync-test-framework"); 4 | 5 | async function testRepoToContainerSync() { 6 | console.log("🧪 Testing Repository to Container Sync"); 7 | console.log("======================================"); 8 | 9 | const framework = new SyncTestFramework(); 10 | 11 | try { 12 | await framework.setup(); 13 | 14 | console.log( 15 | "\n🔍 Step 1: Verifying ONE-TO-ONE sync from test repo to container...", 16 | ); 17 | 18 | // Get list of files in the original test repo 19 | const repoFiles = await framework.listRepoFiles(); 20 | console.log( 21 | `📂 Test repository contains ${repoFiles.length} files:`, 22 | repoFiles, 23 | ); 24 | 25 | // Get list of files in the container 26 | const containerFiles = await framework.listContainerFiles(); 27 | console.log( 28 | `🐳 Container contains ${containerFiles.length} files:`, 29 | containerFiles, 30 | ); 31 | 32 | // CRITICAL: Check for exact one-to-one match 33 | if (containerFiles.length !== repoFiles.length) { 34 | throw new Error( 35 | `File count mismatch! Repo has ${repoFiles.length} files but container has ${containerFiles.length} files`, 36 | ); 37 | } 38 | 39 | // Check that all repo files exist in container 40 | const missingInContainer = []; 41 | for (const file of repoFiles) { 42 | const exists = await framework.containerFileExists(file); 43 | if (!exists) { 44 | missingInContainer.push(file); 45 | } 46 | } 47 | 48 | if (missingInContainer.length > 0) { 49 | throw new Error( 50 | `Files missing in container: ${missingInContainer.join(", ")}`, 51 | ); 52 | } 53 | 54 | // Check for extra files in container that aren't in repo 55 | const extraInContainer = []; 56 | for (const file of containerFiles) { 57 | if (!repoFiles.includes(file)) { 58 | extraInContainer.push(file); 59 | } 60 | } 61 | 62 | if (extraInContainer.length > 0) { 63 | throw new Error( 64 | `Extra files found in container that aren't in test repo: ${extraInContainer.join(", ")}`, 65 | ); 66 | } 67 | 68 | console.log( 69 | "✅ Perfect one-to-one sync: All repo files exist in container, no extra files", 70 | ); 71 | 72 | console.log("\n🔍 Step 2: Verifying file content integrity..."); 73 | 74 | // Check content of key files 75 | const testFiles = [ 76 | "README.md", 77 | "package.json", 78 | "src/main.js", 79 | "src/utils.js", 80 | ]; 81 | const contentMismatches = []; 82 | 83 | for (const file of testFiles) { 84 | if (repoFiles.includes(file)) { 85 | try { 86 | // Read from original repo 87 | const repoContent = await require("fs").promises.readFile( 88 | require("path").join(framework.testRepo, file), 89 | "utf8", 90 | ); 91 | 92 | // Read from container 93 | const containerContent = 94 | await framework.getContainerFileContent(file); 95 | 96 | if (repoContent.trim() !== containerContent.trim()) { 97 | contentMismatches.push({ 98 | file, 99 | repoLength: repoContent.length, 100 | containerLength: containerContent.length, 101 | }); 102 | } 103 | } catch (error) { 104 | contentMismatches.push({ 105 | file, 106 | error: error.message, 107 | }); 108 | } 109 | } 110 | } 111 | 112 | if (contentMismatches.length > 0) { 113 | console.log("Content mismatches found:"); 114 | contentMismatches.forEach((mismatch) => { 115 | if (mismatch.error) { 116 | console.log(` ${mismatch.file}: ${mismatch.error}`); 117 | } else { 118 | console.log( 119 | ` ${mismatch.file}: repo ${mismatch.repoLength} bytes vs container ${mismatch.containerLength} bytes`, 120 | ); 121 | } 122 | }); 123 | throw new Error("File content integrity check failed"); 124 | } 125 | 126 | console.log("✅ File content integrity verified"); 127 | 128 | console.log( 129 | "\n🔍 Step 3: Verifying no extra files from outside test repo...", 130 | ); 131 | 132 | // List of common files that might accidentally be included but shouldn't be 133 | const shouldNotExist = [ 134 | "node_modules", 135 | ".env", 136 | "dist", 137 | "build", 138 | ".vscode", 139 | ".idea", 140 | "coverage", 141 | ]; 142 | 143 | for (const file of shouldNotExist) { 144 | const exists = await framework.containerFileExists(file); 145 | if (exists) { 146 | throw new Error( 147 | `Unexpected file/directory found in container: ${file} (should only contain test repo files)`, 148 | ); 149 | } 150 | } 151 | 152 | console.log( 153 | "✅ No unexpected files found - container only contains test repo files", 154 | ); 155 | 156 | console.log("\n🔍 Step 4: Verifying git repository state..."); 157 | 158 | // Check that git repository exists in container 159 | const gitExists = await framework.containerFileExists(".git/HEAD"); 160 | if (!gitExists) { 161 | throw new Error("Git repository not properly copied to container"); 162 | } 163 | 164 | console.log("✅ Git repository state verified"); 165 | 166 | console.log("\n🔍 Step 5: Verifying working directory setup..."); 167 | 168 | // Check that we're on the correct branch 169 | const { stdout: branchOutput } = await require("util").promisify( 170 | require("child_process").exec, 171 | )( 172 | `docker exec ${framework.containerId} git -C /workspace branch --show-current`, 173 | ); 174 | 175 | const currentBranch = branchOutput.trim(); 176 | if (!currentBranch.startsWith("claude/")) { 177 | throw new Error( 178 | `Expected to be on a claude/ branch, but on: ${currentBranch}`, 179 | ); 180 | } 181 | 182 | console.log(`✅ Working on correct branch: ${currentBranch}`); 183 | 184 | console.log("\n🔍 Step 6: Testing file operations work correctly..."); 185 | 186 | // Test that we can create files in the container 187 | await framework.addFile("test-creation.txt", "Test content"); 188 | await framework.waitForSync(); 189 | 190 | const createdExists = 191 | await framework.containerFileExists("test-creation.txt"); 192 | if (!createdExists) { 193 | throw new Error("File creation in container failed"); 194 | } 195 | 196 | // Test that the file also appears in shadow repo 197 | const shadowExists = await framework.shadowFileExists("test-creation.txt"); 198 | if (!shadowExists) { 199 | throw new Error("File not synced to shadow repository"); 200 | } 201 | 202 | console.log("✅ File operations working correctly"); 203 | 204 | console.log( 205 | "\n🎉 SUCCESS: Repository to container sync is working perfectly!", 206 | ); 207 | 208 | console.log("\n📊 Summary:"); 209 | console.log( 210 | ` 📁 ${repoFiles.length} files synced from test repository to container`, 211 | ); 212 | console.log(` ✅ One-to-one sync verified - no extra files`); 213 | console.log(` ✅ All files exist with correct content`); 214 | console.log(` 🌿 Git repository properly initialized`); 215 | console.log(` 🔄 Bidirectional sync operational`); 216 | } catch (error) { 217 | console.log(`\n❌ FAILED: ${error.message}`); 218 | throw error; 219 | } finally { 220 | await framework.cleanup(); 221 | } 222 | } 223 | 224 | testRepoToContainerSync() 225 | .then(() => { 226 | console.log( 227 | "\n✅ Repository to container sync test completed successfully", 228 | ); 229 | process.exit(0); 230 | }) 231 | .catch((error) => { 232 | console.error( 233 | "\n❌ Repository to container sync test failed:", 234 | error.message, 235 | ); 236 | process.exit(1); 237 | }); 238 | -------------------------------------------------------------------------------- /docs/claude-code-docs/SDK.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "SDK" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/sdk" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-26 8 | description: "Programmatically integrate Claude Code into your applications using the SDK." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | The Claude Code SDK allows developers to programmatically integrate Claude Code into their applications. It enables running Claude Code as a subprocess, providing a way to build AI-powered coding assistants and tools that leverage Claude’s capabilities. 14 | 15 | The SDK currently support command line usage. TypeScript and Python SDKs are coming soon. 16 | 17 | ## Basic SDK usage 18 | 19 | The Claude Code SDK allows you to use Claude Code in non-interactive mode from your applications. Here’s a basic example: 20 | 21 | ## Advanced usage 22 | 23 | ### Multi-turn conversations 24 | 25 | For multi-turn conversations, you can resume conversations or continue from the most recent session: 26 | 27 | ### Custom system prompts 28 | 29 | You can provide custom system prompts to guide Claude’s behavior: 30 | 31 | You can also append instructions to the default system prompt: 32 | 33 | ### MCP Configuration 34 | 35 | The Model Context Protocol (MCP) allows you to extend Claude Code with additional tools and resources from external servers. Using the `--mcp-config` flag, you can load MCP servers that provide specialized capabilities like database access, API integrations, or custom tooling. 36 | 37 | Create a JSON configuration file with your MCP servers: 38 | 39 | Then use it with Claude Code: 40 | 41 | Note: When using MCP tools, you must explicitly allow them using the `--allowedTools` flag. MCP tool names follow the pattern `mcp____` where: 42 | 43 | - `serverName` is the key from your MCP configuration file 44 | - `toolName` is the specific tool provided by that server 45 | 46 | This security measure ensures that MCP tools are only used when explicitly permitted. 47 | 48 | ## Available CLI options 49 | 50 | The SDK leverages all the CLI options available in Claude Code. Here are the key ones for SDK usage: 51 | 52 | | Flag | Description | Example | 53 | | -------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------- | 54 | | `--print`, `-p` | Run in non-interactive mode | `claude -p "query"` | 55 | | `--output-format` | Specify output format (`text`, `json`, `stream-json`) | `claude -p --output-format json` | 56 | | `--resume`, `-r` | Resume a conversation by session ID | `claude --resume abc123` | 57 | | `--continue`, `-c` | Continue the most recent conversation | `claude --continue` | 58 | | `--verbose` | Enable verbose logging | `claude --verbose` | 59 | | `--max-turns` | Limit agentic turns in non-interactive mode | `claude --max-turns 3` | 60 | | `--system-prompt` | Override system prompt (only with `--print`) | `claude --system-prompt "Custom instruction"` | 61 | | `--append-system-prompt` | Append to system prompt (only with `--print`) | `claude --append-system-prompt "Custom instruction"` | 62 | | `--allowedTools` | Comma/space-separated list of allowed tools (includes MCP tools) | `claude --allowedTools "Bash(npm install),mcp__filesystem__*"` | 63 | | `--disallowedTools` | Comma/space-separated list of denied tools | `claude --disallowedTools "Bash(git commit),mcp__github__*"` | 64 | | `--mcp-config` | Load MCP servers from a JSON file | `claude --mcp-config servers.json` | 65 | | `--permission-prompt-tool` | MCP tool for handling permission prompts (only with `--print`) | `claude --permission-prompt-tool mcp__auth__prompt` | 66 | 67 | For a complete list of CLI options and features, see the [CLI usage](https://docs.anthropic.com/en/docs/claude-code/cli-usage) documentation. 68 | 69 | ## Output formats 70 | 71 | The SDK supports multiple output formats: 72 | 73 | ### Text output (default) 74 | 75 | Returns just the response text: 76 | 77 | ### JSON output 78 | 79 | Returns structured data including metadata: 80 | 81 | Response format: 82 | 83 | ### Streaming JSON output 84 | 85 | Streams each message as it is received: 86 | 87 | Each conversation begins with an initial `init` system message, followed by a list of user and assistant messages, followed by a final `result` system message with stats. Each message is emitted as a separate JSON object. 88 | 89 | ## Message schema 90 | 91 | Messages returned from the JSON API are strictly typed according to the following schema: 92 | 93 | ```ts 94 | type Message = 95 | // An assistant message 96 | | { 97 | type: "assistant"; 98 | message: APIAssistantMessage; // from Anthropic SDK 99 | session_id: string; 100 | } 101 | 102 | // A user message 103 | | { 104 | type: "user"; 105 | message: APIUserMessage; // from Anthropic SDK 106 | session_id: string; 107 | } 108 | 109 | // Emitted as the last message 110 | | { 111 | type: "result"; 112 | subtype: "success"; 113 | cost_usd: float; 114 | duration_ms: float; 115 | duration_api_ms: float; 116 | is_error: boolean; 117 | num_turns: int; 118 | result: string; 119 | session_id: string; 120 | } 121 | 122 | // Emitted as the last message, when we've reached the maximum number of turns 123 | | { 124 | type: "result"; 125 | subtype: "error_max_turns"; 126 | cost_usd: float; 127 | duration_ms: float; 128 | duration_api_ms: float; 129 | is_error: boolean; 130 | num_turns: int; 131 | session_id: string; 132 | } 133 | 134 | // Emitted as the first message at the start of a conversation 135 | | { 136 | type: "system"; 137 | subtype: "init"; 138 | session_id: string; 139 | tools: string[]; 140 | mcp_servers: { 141 | name: string; 142 | status: string; 143 | }[]; 144 | }; 145 | ``` 146 | 147 | We will soon publish these types in a JSONSchema-compatible format. We use semantic versioning for the main Claude Code package to communicate breaking changes to this format. 148 | 149 | ## Examples 150 | 151 | ### Simple script integration 152 | 153 | ### Processing files with Claude 154 | 155 | ### Session management 156 | 157 | ## Best practices 158 | 159 | 1. **Use JSON output format** for programmatic parsing of responses: 160 | 2. **Handle errors gracefully** - check exit codes and stderr: 161 | 3. **Use session management** for maintaining context in multi-turn conversations 162 | 4. **Consider timeouts** for long-running operations: 163 | 5. **Respect rate limits** when making multiple requests by adding delays between calls 164 | 165 | ## Real-world applications 166 | 167 | The Claude Code SDK enables powerful integrations with your development workflow. One notable example is the [Claude Code GitHub Actions](https://docs.anthropic.com/en/docs/claude-code/github-actions), which uses the SDK to provide automated code review, PR creation, and issue triage capabilities directly in your GitHub workflow. 168 | 169 | - [CLI usage and controls](https://docs.anthropic.com/en/docs/claude-code/cli-usage) - Complete CLI documentation 170 | - [GitHub Actions integration](https://docs.anthropic.com/en/docs/claude-code/github-actions) - Automate your GitHub workflow with Claude 171 | - [Tutorials](https://docs.anthropic.com/en/docs/claude-code/tutorials) - Step-by-step guides for common use cases 172 | -------------------------------------------------------------------------------- /docs/claude-code-docs/Bedrock, Vertex, and proxies.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Bedrock, Vertex, and proxies" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/bedrock-vertex-proxies" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Configure Claude Code to work with Amazon Bedrock and Google Vertex AI, and connect through proxies." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | ## Model configuration 14 | 15 | Claude Code uses the following defaults: 16 | 17 | | Provider | Default Model | 18 | | ----------------- | -------------------------------------------------------------------------------- | 19 | | Anthropic Console | `claude-sonnet-4-20250514` | 20 | | Claude Max | `claude-opus-4-20250514` or `claude-sonnet-4-20250514` based on Max usage limits | 21 | | Amazon Bedrock | `claude-3-7-sonnet-20250219` | 22 | | Google Vertex AI | `claude-sonnet-4-20250514` | 23 | 24 | The default values can be overridden in several ways based on the following precedence from top to bottom: 25 | 26 | - `--model` CLI flag. Applies within the session only. 27 | - `ANTHROPIC_MODEL` environment variable. Applies within the session only. 28 | - User Settings `~/.claude/settings.json`: Persists across sessions. 29 | 30 | You can supply a full model name, the alias `sonnet` for the latest Claude Sonnet model for your provider, or the alias `opus` for the latest Claude Opus model for your provider. 31 | 32 | See our [model names reference](https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-names) for all available models across different providers. 33 | 34 | ## Use with third-party APIs 35 | 36 | Claude Code requires access to both Claude Sonnet 3.7 and Claude Haiku 3.5 models, regardless of which API provider you use. 37 | 38 | ### Connect to Amazon Bedrock 39 | 40 | ```bash 41 | CLAUDE_CODE_USE_BEDROCK=1 42 | ``` 43 | 44 | If you don’t have prompt caching enabled, also set: 45 | 46 | ```bash 47 | DISABLE_PROMPT_CACHING=1 48 | ``` 49 | 50 | Contact Amazon Bedrock for prompt caching for reduced costs and higher rate limits. 51 | 52 | Requires standard AWS SDK credentials (e.g., `~/.aws/credentials` or relevant environment variables like `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). To set up AWS credentials, run: 53 | 54 | If you’d like to access Claude Code via proxy, you can use the `ANTHROPIC_BEDROCK_BASE_URL` environment variable: 55 | 56 | ```bash 57 | ANTHROPIC_BEDROCK_BASE_URL='https://your-proxy-url' 58 | ``` 59 | 60 | If your proxy maintains its own AWS credentials, you can use the `CLAUDE_CODE_SKIP_BEDROCK_AUTH` environment variable to remove Claude Code’s requirement for AWS credentials. 61 | 62 | ```bash 63 | CLAUDE_CODE_SKIP_BEDROCK_AUTH=1 64 | ``` 65 | 66 | Users will need access to both Claude Sonnet 3.7 and Claude Haiku 3.5 models in their AWS account. If you have a model access role, you may need to request access to these models if they’re not already available. Access to Bedrock in each region is necessary because inference profiles require cross-region capability. 67 | 68 | ### Connect to Google Vertex AI 69 | 70 | If you don’t have prompt caching enabled, also set: 71 | 72 | ```bash 73 | DISABLE_PROMPT_CACHING=1 74 | ``` 75 | 76 | Claude Code on Vertex AI currently only supports the `us-east5` region. Make sure your project has quota allocated in this specific region. 77 | 78 | Users will need access to both Claude Sonnet 3.7 and Claude Haiku 3.5 models in their Vertex AI project. 79 | 80 | Requires standard GCP credentials configured through google-auth-library. To set up GCP credentials, run: 81 | 82 | If you’d like to access Claude Code via proxy, you can use the `ANTHROPIC_VERTEX_BASE_URL` environment variable: 83 | 84 | ```bash 85 | ANTHROPIC_VERTEX_BASE_URL='https://your-proxy-url' 86 | ``` 87 | 88 | If your proxy maintains its own GCP credentials, you can use the `CLAUDE_CODE_SKIP_VERTEX_AUTH` environment variable to remove Claude Code’s requirement for GCP credentials. 89 | 90 | ```bash 91 | CLAUDE_CODE_SKIP_VERTEX_AUTH=1 92 | ``` 93 | 94 | For the best experience, contact Google for heightened rate limits. 95 | 96 | ## Connect through a proxy 97 | 98 | When using Claude Code with an LLM proxy, you can control authentication behavior using the following environment variables and configs. Note that you can mix and match these with Bedrock and Vertex-specific settings. 99 | 100 | ### Settings 101 | 102 | Claude Code supports a number of settings controlled via environment variables to configure usage with Bedrock and Vertex. See [Environment variables](https://docs.anthropic.com/en/docs/claude-code/settings#environment-variables) for a complete reference. 103 | 104 | If you prefer to configure via a file instead of environment variables, you can add any of these settings to the `env` object in your [Claude Code settings](https://docs.anthropic.com/en/docs/claude-code/settings#available-settings) files. 105 | 106 | You can also configure the `apiKeyHelper` setting, to set a custom shell script to get an API key (invoked once at startup, and cached for the duration of each session, or until `CLAUDE_CODE_API_KEY_HELPER_TTL_MS` elapses). 107 | 108 | ### LiteLLM 109 | 110 | LiteLLM is a third-party proxy service. Anthropic doesn’t endorse, maintain, or audit LiteLLM’s security or functionality. This guide is provided for informational purposes and may become outdated. Use at your own discretion. 111 | 112 | This section shows configuration of Claude Code with LiteLLM Proxy Server, a third-party LLM proxy which offers usage and spend tracking, centralized authentication, per-user budgeting, and more. 113 | 114 | #### Step 1: Prerequisites 115 | 116 | - Claude Code updated to the latest version 117 | - LiteLLM Proxy Server running and network-accessible to Claude Code 118 | - Your LiteLLM proxy key 119 | 120 | #### Step 2: Set up proxy authentication 121 | 122 | Choose one of these authentication methods: 123 | 124 | **Option A: Static proxy key** Set your proxy key as an environment variable: 125 | 126 | ```bash 127 | ANTHROPIC_AUTH_TOKEN=your-proxy-key 128 | ``` 129 | 130 | **Option B: Dynamic proxy key** If your organization uses rotating keys or dynamic authentication: 131 | 132 | 1. Do not set the `ANTHROPIC_AUTH_TOKEN` environment variable 133 | 2. Author a key helper script to provide authentication tokens 134 | 3. Register the script under `apiKeyHelper` configuration in your Claude Code settings 135 | 4. Set the token lifetime to enable automatic refresh: 136 | ```bash 137 | CLAUDE_CODE_API_KEY_HELPER_TTL_MS=3600000 138 | ``` 139 | Set this to the lifetime (in milliseconds) of tokens returned by your `apiKeyHelper`. 140 | 141 | #### Step 3: Configure your deployment 142 | 143 | Choose which Claude deployment you want to use through LiteLLM: 144 | 145 | - **Anthropic API**: Direct connection to Anthropic’s API 146 | - **Bedrock**: Amazon Bedrock with Claude models 147 | - **Vertex AI**: Google Cloud Vertex AI with Claude models 148 | 149 | ##### Option A: Anthropic API through LiteLLM 150 | 151 | 1. Configure the LiteLLM endpoint: 152 | ```bash 153 | ANTHROPIC_BASE_URL=https://litellm-url:4000/anthropic 154 | ``` 155 | 156 | ##### Option B: Bedrock through LiteLLM 157 | 158 | 1. Configure Bedrock settings: 159 | 160 | ##### Option C: Vertex AI through LiteLLM 161 | 162 | **Recommended: Proxy-specified credentials** 163 | 164 | 1. Configure Vertex settings: 165 | 166 | **Alternative: Client-specified credentials** 167 | 168 | If you prefer to use local GCP credentials: 169 | 170 | 1. Authenticate with GCP locally: 171 | 2. Configure Vertex settings: 172 | 3. Update LiteLLM header configuration: 173 | Ensure your LiteLLM config has `general_settings.litellm_key_header_name` set to `Proxy-Authorization`, since the pass-through GCP token will be located on the `Authorization` header. 174 | 175 | #### Step 4. Selecting a model 176 | 177 | By default, the models will use those specified in [Model configuration](https://docs.anthropic.com/en/docs/claude-code/bedrock-vertex-proxies#model-configuration). 178 | 179 | If you have configured custom model names in LiteLLM, set the aforementioned environment variables to those custom names. 180 | 181 | For more detailed information, refer to the [LiteLLM documentation](https://docs.litellm.ai/). 182 | -------------------------------------------------------------------------------- /src/web-server-attach.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { createServer } from "http"; 3 | import { Server } from "socket.io"; 4 | import path from "path"; 5 | import Docker from "dockerode"; 6 | import chalk from "chalk"; 7 | 8 | interface SessionInfo { 9 | containerId: string; 10 | stream?: any; 11 | connectedSockets: Set; 12 | } 13 | 14 | export class WebUIServer { 15 | private app: express.Application; 16 | private httpServer: any; 17 | private io: Server; 18 | private docker: Docker; 19 | private sessions: Map = new Map(); 20 | private port: number = 3456; 21 | 22 | constructor(docker: Docker) { 23 | this.docker = docker; 24 | this.app = express(); 25 | this.httpServer = createServer(this.app); 26 | this.io = new Server(this.httpServer, { 27 | cors: { 28 | origin: "*", 29 | methods: ["GET", "POST"], 30 | }, 31 | }); 32 | 33 | this.setupRoutes(); 34 | this.setupSocketHandlers(); 35 | } 36 | 37 | private setupRoutes(): void { 38 | // Serve static files 39 | this.app.use(express.static(path.join(__dirname, "../public"))); 40 | 41 | // Health check endpoint 42 | this.app.get("/api/health", (_req, res) => { 43 | res.json({ status: "ok" }); 44 | }); 45 | 46 | // Container info endpoint 47 | this.app.get("/api/containers", async (_req, res) => { 48 | try { 49 | const containers = await this.docker.listContainers(); 50 | const claudeContainers = containers.filter((c) => 51 | c.Names.some((name) => name.includes("claude-code-sandbox")), 52 | ); 53 | res.json(claudeContainers); 54 | } catch (error) { 55 | res.status(500).json({ error: "Failed to list containers" }); 56 | } 57 | }); 58 | } 59 | 60 | private setupSocketHandlers(): void { 61 | this.io.on("connection", (socket) => { 62 | console.log(chalk.blue("✓ Client connected to web UI")); 63 | 64 | socket.on("attach", async (data) => { 65 | const { containerId } = data; 66 | 67 | try { 68 | const container = this.docker.getContainer(containerId); 69 | 70 | // Check if we already have a session for this container 71 | let session = this.sessions.get(containerId); 72 | 73 | if (!session || !session.stream) { 74 | // Attach to the container's main process 75 | console.log(chalk.blue("Attaching to container...")); 76 | 77 | const stream = await container.attach({ 78 | stream: true, 79 | stdin: true, 80 | stdout: true, 81 | stderr: true, 82 | hijack: true, 83 | }); 84 | 85 | session = { 86 | containerId, 87 | stream, 88 | connectedSockets: new Set([socket.id]), 89 | }; 90 | this.sessions.set(containerId, session); 91 | 92 | // Set up stream handlers 93 | stream.on("data", (chunk: Buffer) => { 94 | // Docker attach streams don't have the same header format as exec 95 | // Just forward the data as-is 96 | if (chunk.length > 0) { 97 | for (const socketId of session!.connectedSockets) { 98 | const connectedSocket = this.io.sockets.sockets.get(socketId); 99 | if (connectedSocket) { 100 | connectedSocket.emit("output", new Uint8Array(chunk)); 101 | } 102 | } 103 | } 104 | }); 105 | 106 | stream.on("error", (err: Error) => { 107 | console.error(chalk.red("Stream error:"), err); 108 | for (const socketId of session!.connectedSockets) { 109 | const connectedSocket = this.io.sockets.sockets.get(socketId); 110 | if (connectedSocket) { 111 | connectedSocket.emit("error", { message: err.message }); 112 | } 113 | } 114 | }); 115 | 116 | stream.on("end", () => { 117 | for (const socketId of session!.connectedSockets) { 118 | const connectedSocket = this.io.sockets.sockets.get(socketId); 119 | if (connectedSocket) { 120 | connectedSocket.emit("container-disconnected"); 121 | } 122 | } 123 | this.sessions.delete(containerId); 124 | }); 125 | 126 | console.log(chalk.green("Attached to container")); 127 | } else { 128 | // Add this socket to the existing session 129 | console.log(chalk.blue("Reconnecting to existing session")); 130 | session.connectedSockets.add(socket.id); 131 | } 132 | 133 | // Confirm attachment 134 | socket.emit("attached", { containerId }); 135 | 136 | // Container attach doesn't support resize like exec does 137 | // But we can try to send a resize sequence through stdin 138 | if (data.cols && data.rows) { 139 | setTimeout(() => { 140 | // Send terminal resize escape sequence 141 | const resizeSeq = `\x1b[8;${data.rows};${data.cols}t`; 142 | if (session && session.stream) { 143 | session.stream.write(resizeSeq); 144 | } 145 | }, 100); 146 | } 147 | } catch (error: any) { 148 | console.error(chalk.red("Failed to attach to container:"), error); 149 | socket.emit("error", { message: error.message }); 150 | } 151 | }); 152 | 153 | socket.on("resize", async (data) => { 154 | const { cols, rows } = data; 155 | 156 | // Find which session this socket belongs to 157 | for (const [, session] of this.sessions) { 158 | if (session.connectedSockets.has(socket.id) && session.stream) { 159 | // Send resize escape sequence 160 | const resizeSeq = `\x1b[8;${rows};${cols}t`; 161 | session.stream.write(resizeSeq); 162 | break; 163 | } 164 | } 165 | }); 166 | 167 | socket.on("input", (data) => { 168 | // Find which session this socket belongs to 169 | for (const [, session] of this.sessions) { 170 | if (session.connectedSockets.has(socket.id) && session.stream) { 171 | session.stream.write(data); 172 | break; 173 | } 174 | } 175 | }); 176 | 177 | socket.on("disconnect", () => { 178 | console.log(chalk.yellow("Client disconnected from web UI")); 179 | 180 | // Remove socket from all sessions but don't close the stream 181 | for (const [, session] of this.sessions) { 182 | session.connectedSockets.delete(socket.id); 183 | } 184 | }); 185 | }); 186 | } 187 | 188 | async start(): Promise { 189 | return new Promise((resolve, reject) => { 190 | this.httpServer.listen(this.port, () => { 191 | const url = `http://localhost:${this.port}`; 192 | console.log(chalk.green(`✓ Web UI server started at ${url}`)); 193 | resolve(url); 194 | }); 195 | 196 | this.httpServer.on("error", (err: any) => { 197 | if (err.code === "EADDRINUSE") { 198 | this.port++; 199 | this.httpServer.listen(this.port, () => { 200 | const url = `http://localhost:${this.port}`; 201 | console.log(chalk.green(`✓ Web UI server started at ${url}`)); 202 | resolve(url); 203 | }); 204 | } else { 205 | reject(err); 206 | } 207 | }); 208 | }); 209 | } 210 | 211 | async stop(): Promise { 212 | // Clean up all sessions 213 | for (const [, session] of this.sessions) { 214 | if (session.stream) { 215 | session.stream.end(); 216 | } 217 | } 218 | this.sessions.clear(); 219 | 220 | // Close socket.io connections 221 | this.io.close(); 222 | 223 | // Close HTTP server 224 | return new Promise((resolve) => { 225 | this.httpServer.close(() => { 226 | console.log(chalk.yellow("Web UI server stopped")); 227 | resolve(); 228 | }); 229 | }); 230 | } 231 | 232 | async openInBrowser(url: string): Promise { 233 | try { 234 | const open = (await import("open")).default; 235 | await open(url); 236 | console.log(chalk.blue("✓ Opened browser")); 237 | } catch (error) { 238 | console.log(chalk.yellow("Could not open browser automatically")); 239 | console.log(chalk.yellow(`Please open ${url} in your browser`)); 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /docs/claude-code-docs/Monitoring usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Monitoring usage" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/monitoring-usage" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Monitor Claude Code usage with OpenTelemetry metrics" 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | OpenTelemetry support is currently in beta and details are subject to change. 14 | 15 | ## OpenTelemetry in Claude Code 16 | 17 | Claude Code supports OpenTelemetry (OTel) metrics for monitoring and observability. This document explains how to enable and configure OTel for Claude Code. 18 | 19 | All metrics are time series data exported via OpenTelemetry’s standard metrics protocol. It is the user’s responsibility to ensure their metrics backend is properly configured and that the aggregation granularity meets their monitoring requirements. 20 | 21 | ## Quick Start 22 | 23 | Configure OpenTelemetry using environment variables: 24 | 25 | The default export interval is 10 minutes. During setup, you may want to use a shorter interval for debugging purposes. Remember to reset this for production use. 26 | 27 | For full configuration options, see the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options). 28 | 29 | ## Administrator Configuration 30 | 31 | Administrators can configure OpenTelemetry settings for all users through the managed settings file. This allows for centralized control of telemetry settings across an organization. See the [configuration hierarchy](https://docs.anthropic.com/en/docs/claude-code/settings#configuration-hierarchy) for more information about how settings are applied. 32 | 33 | The managed settings file is located at: 34 | 35 | - macOS: `/Library/Application Support/ClaudeCode/managed-settings.json` 36 | - Linux: `/etc/claude-code/managed-settings.json` 37 | 38 | Example managed settings configuration: 39 | 40 | Managed settings can be distributed via MDM (Mobile Device Management) or other device management solutions. Environment variables defined in the managed settings file have high precedence and cannot be overridden by users. 41 | 42 | ## Configuration Details 43 | 44 | ### Common Configuration Variables 45 | 46 | | Environment Variable | Description | Example Values | 47 | | ----------------------------------------------- | ------------------------------------------------ | ------------------------------------ | 48 | | `CLAUDE_CODE_ENABLE_TELEMETRY` | Enables telemetry collection (required) | `1` | 49 | | `OTEL_METRICS_EXPORTER` | Exporter type(s) to use (comma-separated) | `console`, `otlp`, `prometheus` | 50 | | `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol for OTLP exporter | `grpc`, `http/json`, `http/protobuf` | 51 | | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | `http://localhost:4317` | 52 | | `OTEL_EXPORTER_OTLP_HEADERS` | Authentication headers for OTLP | `Authorization=Bearer token` | 53 | | `OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY` | Client key for mTLS authentication | Path to client key file | 54 | | `OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE` | Client certificate for mTLS authentication | Path to client cert file | 55 | | `OTEL_METRIC_EXPORT_INTERVAL` | Export interval in milliseconds (default: 10000) | `5000`, `60000` | 56 | 57 | ### Metrics Cardinality Control 58 | 59 | The following environment variables control which attributes are included in metrics to manage cardinality: 60 | 61 | | Environment Variable | Description | Default Value | Example to Disable | 62 | | ----------------------------------- | ---------------------------------------------- | ------------- | ------------------ | 63 | | `OTEL_METRICS_INCLUDE_SESSION_ID` | Include session.id attribute in metrics | `true` | `false` | 64 | | `OTEL_METRICS_INCLUDE_VERSION` | Include app.version attribute in metrics | `false` | `true` | 65 | | `OTEL_METRICS_INCLUDE_ACCOUNT_UUID` | Include user.account_uuid attribute in metrics | `true` | `false` | 66 | 67 | These variables help control the cardinality of metrics, which affects storage requirements and query performance in your metrics backend. Lower cardinality generally means better performance and lower storage costs but less granular data for analysis. 68 | 69 | ### Example Configurations 70 | 71 | ## Available Metrics 72 | 73 | Claude Code exports the following metrics: 74 | 75 | | Metric Name | Description | Unit | 76 | | --------------------------------- | ------------------------------- | ------ | 77 | | `claude_code.session.count` | Count of CLI sessions started | count | 78 | | `claude_code.lines_of_code.count` | Count of lines of code modified | count | 79 | | `claude_code.pull_request.count` | Number of pull requests created | count | 80 | | `claude_code.commit.count` | Number of git commits created | count | 81 | | `claude_code.cost.usage` | Cost of the Claude Code session | USD | 82 | | `claude_code.token.usage` | Number of tokens used | tokens | 83 | 84 | ### Metric Details 85 | 86 | All metrics share these standard attributes: 87 | 88 | - `session.id`: Unique session identifier (controlled by `OTEL_METRICS_INCLUDE_SESSION_ID`) 89 | - `app.version`: Current Claude Code version (controlled by `OTEL_METRICS_INCLUDE_VERSION`) 90 | - `organization.id`: Organization UUID (when authenticated) 91 | - `user.account_uuid`: Account UUID (when authenticated, controlled by `OTEL_METRICS_INCLUDE_ACCOUNT_UUID`) 92 | 93 | #### 1\. Session Counter 94 | 95 | Emitted at the start of each session. 96 | 97 | #### 2\. Lines of Code Counter 98 | 99 | Emitted when code is added or removed. 100 | 101 | - Additional attribute: `type` (`"added"` or `"removed"`) 102 | 103 | #### 3\. Pull Request Counter 104 | 105 | Emitted when creating pull requests via Claude Code. 106 | 107 | #### 4\. Commit Counter 108 | 109 | Emitted when creating git commits via Claude Code. 110 | 111 | #### 5\. Cost Counter 112 | 113 | Emitted after each API request. 114 | 115 | - Additional attribute: `model` 116 | 117 | #### 6\. Token Counter 118 | 119 | Emitted after each API request. 120 | 121 | - Additional attributes: `type` (`"input"`, `"output"`, `"cacheRead"`, `"cacheCreation"`) and `model` 122 | 123 | ## Interpreting Metrics Data 124 | 125 | These metrics provide insights into usage patterns, productivity, and costs: 126 | 127 | ### Usage Monitoring 128 | 129 | | Metric | Analysis Opportunity | 130 | | ------------------------------------------------------------- | --------------------------------------------------------- | 131 | | `claude_code.token.usage` | Break down by `type` (input/output), user, team, or model | 132 | | `claude_code.session.count` | Track adoption and engagement over time | 133 | | `claude_code.lines_of_code.count` | Measure productivity by tracking code additions/removals | 134 | | `claude_code.commit.count` & `claude_code.pull_request.count` | Understand impact on development workflows | 135 | 136 | ### Cost Monitoring 137 | 138 | The `claude_code.cost.usage` metric helps with: 139 | 140 | - Tracking usage trends across teams or individuals 141 | - Identifying high-usage sessions for optimization 142 | 143 | Cost metrics are approximations. For official billing data, refer to your API provider (Anthropic Console, AWS Bedrock, or Google Cloud Vertex). 144 | 145 | ### Alerting and Segmentation 146 | 147 | Common alerts to consider: 148 | 149 | - Cost spikes 150 | - Unusual token consumption 151 | - High session volume from specific users 152 | 153 | All metrics can be segmented by `user.account_uuid`, `organization.id`, `session.id`, `model`, and `app.version`. 154 | 155 | ## Backend Considerations 156 | 157 | | Backend Type | Best For | 158 | | -------------------------------------------- | ------------------------------------------ | 159 | | Time series databases (Prometheus) | Rate calculations, aggregated metrics | 160 | | Columnar stores (ClickHouse) | Complex queries, unique user analysis | 161 | | Observability platforms (Honeycomb, Datadog) | Advanced querying, visualization, alerting | 162 | 163 | For DAU/WAU/MAU metrics, choose backends that support efficient unique value queries. 164 | 165 | ## Service Information 166 | 167 | All metrics are exported with: 168 | 169 | - Service Name: `claude-code` 170 | - Service Version: Current Claude Code version 171 | - Meter Name: `com.anthropic.claude_code` 172 | 173 | ## Security Considerations 174 | 175 | - Telemetry is opt-in and requires explicit configuration 176 | - Sensitive information like API keys or file contents are never included in metrics 177 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Docker from "dockerode"; 2 | import { simpleGit, SimpleGit } from "simple-git"; 3 | import chalk from "chalk"; 4 | import { CredentialManager } from "./credentials"; 5 | import { GitMonitor } from "./git-monitor"; 6 | import { ContainerManager } from "./container"; 7 | import { UIManager } from "./ui"; 8 | import { WebUIServer } from "./web-server"; 9 | import { SandboxConfig } from "./types"; 10 | import { getDockerConfig, isPodman } from "./docker-config"; 11 | import path from "path"; 12 | 13 | export class ClaudeSandbox { 14 | private docker: Docker; 15 | private git: SimpleGit; 16 | private config: SandboxConfig; 17 | private credentialManager: CredentialManager; 18 | private gitMonitor: GitMonitor; 19 | private containerManager: ContainerManager; 20 | private ui: UIManager; 21 | private webServer?: WebUIServer; 22 | 23 | constructor(config: SandboxConfig) { 24 | this.config = config; 25 | const dockerConfig = getDockerConfig(config.dockerSocketPath); 26 | this.docker = new Docker(dockerConfig); 27 | 28 | // Log if using Podman 29 | if (isPodman(dockerConfig)) { 30 | console.log(chalk.blue("Detected Podman socket")); 31 | } 32 | 33 | this.git = simpleGit(); 34 | this.credentialManager = new CredentialManager(); 35 | this.gitMonitor = new GitMonitor(this.git); 36 | this.containerManager = new ContainerManager(this.docker, config); 37 | this.ui = new UIManager(); 38 | } 39 | 40 | async run(): Promise { 41 | try { 42 | // Verify we're in a git repository 43 | await this.verifyGitRepo(); 44 | 45 | // Check current branch 46 | const currentBranch = await this.git.branchLocal(); 47 | console.log(chalk.blue(`Current branch: ${currentBranch.current}`)); 48 | 49 | // Determine target branch based on config options (but don't checkout in host repo) 50 | let branchName = ""; 51 | let prFetchRef = ""; 52 | let remoteFetchRef = ""; 53 | 54 | if (this.config.prNumber) { 55 | // Get PR branch name from GitHub but don't checkout locally 56 | console.log(chalk.blue(`Getting PR #${this.config.prNumber} info...`)); 57 | try { 58 | const { execSync } = require("child_process"); 59 | 60 | // Get PR info to find the actual branch name 61 | const prInfo = execSync( 62 | `gh pr view ${this.config.prNumber} --json headRefName`, 63 | { 64 | encoding: "utf-8", 65 | cwd: process.cwd(), 66 | }, 67 | ); 68 | const prData = JSON.parse(prInfo); 69 | branchName = prData.headRefName; 70 | prFetchRef = `pull/${this.config.prNumber}/head:${branchName}`; 71 | 72 | console.log( 73 | chalk.blue( 74 | `PR #${this.config.prNumber} uses branch: ${branchName}`, 75 | ), 76 | ); 77 | console.log( 78 | chalk.blue(`Will setup container with PR branch: ${branchName}`), 79 | ); 80 | } catch (error) { 81 | console.error( 82 | chalk.red(`✗ Failed to get PR #${this.config.prNumber} info:`), 83 | error, 84 | ); 85 | throw error; 86 | } 87 | } else if (this.config.remoteBranch) { 88 | // Parse remote branch but don't checkout locally 89 | console.log( 90 | chalk.blue( 91 | `Will setup container with remote branch: ${this.config.remoteBranch}`, 92 | ), 93 | ); 94 | try { 95 | // Parse remote/branch format 96 | const parts = this.config.remoteBranch.split("/"); 97 | if (parts.length < 2) { 98 | throw new Error( 99 | 'Remote branch must be in format "remote/branch" (e.g., "origin/feature-branch")', 100 | ); 101 | } 102 | 103 | const remote = parts[0]; 104 | const branch = parts.slice(1).join("/"); 105 | 106 | console.log(chalk.blue(`Remote: ${remote}, Branch: ${branch}`)); 107 | branchName = branch; 108 | remoteFetchRef = `${remote}/${branch}`; 109 | } catch (error) { 110 | console.error( 111 | chalk.red( 112 | `✗ Failed to parse remote branch ${this.config.remoteBranch}:`, 113 | ), 114 | error, 115 | ); 116 | throw error; 117 | } 118 | } else { 119 | // Use target branch from config or generate one 120 | branchName = 121 | this.config.targetBranch || 122 | (() => { 123 | const timestamp = new Date() 124 | .toISOString() 125 | .replace(/[:.]/g, "-") 126 | .split("T")[0]; 127 | return `claude/${timestamp}-${Date.now()}`; 128 | })(); 129 | console.log( 130 | chalk.blue(`Will create branch in container: ${branchName}`), 131 | ); 132 | } 133 | 134 | // Discover credentials (optional - don't fail if not found) 135 | const credentials = await this.credentialManager.discover(); 136 | 137 | // Prepare container environment 138 | const containerConfig = await this.prepareContainer( 139 | branchName, 140 | credentials, 141 | prFetchRef, 142 | remoteFetchRef, 143 | ); 144 | 145 | // Start container 146 | const containerId = await this.containerManager.start(containerConfig); 147 | console.log( 148 | chalk.green(`✓ Started container: ${containerId.substring(0, 12)}`), 149 | ); 150 | 151 | // Start monitoring for commits 152 | this.gitMonitor.on("commit", async (commit) => { 153 | await this.handleCommit(commit); 154 | }); 155 | 156 | await this.gitMonitor.start(branchName); 157 | console.log(chalk.blue("✓ Git monitoring started")); 158 | 159 | // Always launch web UI 160 | this.webServer = new WebUIServer(this.docker); 161 | 162 | // Pass repo info to web server 163 | this.webServer.setRepoInfo(process.cwd(), branchName); 164 | 165 | const webUrl = await this.webServer.start(); 166 | 167 | // Open browser to the web UI with container ID 168 | const fullUrl = `${webUrl}?container=${containerId}`; 169 | await this.webServer.openInBrowser(fullUrl); 170 | 171 | console.log(chalk.green(`\n✓ Web UI available at: ${fullUrl}`)); 172 | console.log( 173 | chalk.yellow("Keep this terminal open to maintain the session"), 174 | ); 175 | 176 | // Keep the process running 177 | await new Promise(() => {}); // This will keep the process alive 178 | } catch (error) { 179 | console.error(chalk.red("Error:"), error); 180 | throw error; 181 | } 182 | } 183 | 184 | private async verifyGitRepo(): Promise { 185 | const isRepo = await this.git.checkIsRepo(); 186 | if (!isRepo) { 187 | throw new Error( 188 | "Not a git repository. Please run claude-sandbox from within a git repository.", 189 | ); 190 | } 191 | } 192 | 193 | private async prepareContainer( 194 | branchName: string, 195 | credentials: any, 196 | prFetchRef?: string, 197 | remoteFetchRef?: string, 198 | ): Promise { 199 | const workDir = process.cwd(); 200 | const repoName = path.basename(workDir); 201 | 202 | return { 203 | branchName, 204 | credentials, 205 | workDir, 206 | repoName, 207 | dockerImage: this.config.dockerImage || "claude-sandbox:latest", 208 | prFetchRef, 209 | remoteFetchRef, 210 | }; 211 | } 212 | 213 | private async handleCommit(commit: any): Promise { 214 | // Show commit notification 215 | this.ui.showCommitNotification(commit); 216 | 217 | // Show diff 218 | const diff = await this.git.diff(["HEAD~1", "HEAD"]); 219 | this.ui.showDiff(diff); 220 | 221 | // Ask user what to do 222 | const action = await this.ui.askCommitAction(); 223 | 224 | switch (action) { 225 | case "nothing": 226 | console.log(chalk.blue("Continuing...")); 227 | break; 228 | case "push": 229 | await this.pushBranch(); 230 | break; 231 | case "push-pr": 232 | await this.pushBranchAndCreatePR(); 233 | break; 234 | case "exit": 235 | await this.cleanup(); 236 | process.exit(0); 237 | } 238 | } 239 | 240 | private async pushBranch(): Promise { 241 | const currentBranch = await this.git.branchLocal(); 242 | await this.git.push("origin", currentBranch.current); 243 | console.log(chalk.green(`✓ Pushed branch: ${currentBranch.current}`)); 244 | } 245 | 246 | private async pushBranchAndCreatePR(): Promise { 247 | await this.pushBranch(); 248 | 249 | // Use gh CLI to create PR 250 | const { execSync } = require("child_process"); 251 | try { 252 | execSync("gh pr create --fill", { stdio: "inherit" }); 253 | console.log(chalk.green("✓ Created pull request")); 254 | } catch (error) { 255 | console.error( 256 | chalk.yellow( 257 | "Could not create PR automatically. Please create it manually.", 258 | ), 259 | ); 260 | } 261 | } 262 | 263 | private async cleanup(): Promise { 264 | await this.gitMonitor.stop(); 265 | await this.containerManager.cleanup(); 266 | if (this.webServer) { 267 | await this.webServer.stop(); 268 | } 269 | } 270 | } 271 | 272 | export * from "./types"; 273 | -------------------------------------------------------------------------------- /docs/claude-code-docs/CLI usage and controls.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "CLI usage and controls" 3 | source: "https://docs.anthropic.com/en/docs/claude-code/cli-usage" 4 | author: 5 | - "[[Anthropic]]" 6 | published: 7 | created: 2025-05-25 8 | description: "Learn how to use Claude Code from the command line, including CLI commands, flags, and slash commands." 9 | tags: 10 | - "clippings" 11 | --- 12 | 13 | ## Getting started 14 | 15 | Claude Code provides two main ways to interact: 16 | 17 | - **Interactive mode**: Run `claude` to start a REPL session 18 | - **One-shot mode**: Use `claude -p "query"` for quick commands 19 | 20 | ## CLI commands 21 | 22 | | Command | Description | Example | 23 | | ---------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | 24 | | `claude` | Start interactive REPL | `claude` | 25 | | `claude "query"` | Start REPL with initial prompt | `claude "explain this project"` | 26 | | `claude -p "query"` | Run one-off query, then exit | `claude -p "explain this function"` | 27 | | `cat file \| claude -p "query"` | Process piped content | `cat logs.txt \| claude -p "explain"` | 28 | | `claude -c` | Continue most recent conversation | `claude -c` | 29 | | `claude -c -p "query"` | Continue in print mode | `claude -c -p "Check for type errors"` | 30 | | `claude -r "" "query"` | Resume session by ID | `claude -r "abc123" "Finish this PR"` | 31 | | `claude config` | Configure settings | `claude config set --global theme dark` | 32 | | `claude update` | Update to latest version | `claude update` | 33 | | `claude mcp` | Configure Model Context Protocol servers | [See MCP section in tutorials](https://docs.anthropic.com/en/docs/claude-code/tutorials#set-up-model-context-protocol-mcp) | 34 | 35 | ## CLI flags 36 | 37 | Customize Claude Code’s behavior with these command-line flags: 38 | 39 | | Flag | Description | Example | 40 | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | 41 | | `--print`, `-p` | Print response without interactive mode (see [SDK documentation](https://docs.anthropic.com/en/docs/claude-code/sdk) for programmatic usage details) | `claude -p "query"` | 42 | | `--output-format` | Specify output format for print mode (options: `text`, `json`, `stream-json`) | `claude -p "query" --output-format json` | 43 | | `--verbose` | Enable verbose logging, shows full turn-by-turn output (helpful for debugging in both print and interactive modes) | `claude --verbose` | 44 | | `--max-turns` | Limit the number of agentic turns in non-interactive mode | `claude -p --max-turns 3 "query"` | 45 | | `--model` | Sets the model for the current session with an alias for the latest model (`sonnet` or `opus`) or a model’s full name | `claude --model claude-sonnet-4-20250514` | 46 | | `--permission-prompt-tool` | Specify an MCP tool to handle permission prompts in non-interactive mode | `claude -p --permission-prompt-tool mcp_auth_tool "query"` | 47 | | `--resume` | Resume a specific session by ID, or by choosing in interactive mode | `claude --resume abc123 "query"` | 48 | | `--continue` | Load the most recent conversation in the current directory | `claude --continue` | 49 | | `--dangerously-skip-permissions` | Skip permission prompts (use with caution) | `claude --dangerously-skip-permissions` | 50 | 51 | For detailed information about print mode (`-p`) including output formats, streaming, verbose logging, and programmatic usage, see the [SDK documentation](https://docs.anthropic.com/en/docs/claude-code/sdk). 52 | 53 | ## Slash commands 54 | 55 | Control Claude’s behavior during an interactive session: 56 | 57 | | Command | Purpose | 58 | | ------------------------- | --------------------------------------------------------------------- | 59 | | `/bug` | Report bugs (sends conversation to Anthropic) | 60 | | `/clear` | Clear conversation history | 61 | | `/compact [instructions]` | Compact conversation with optional focus instructions | 62 | | `/config` | View/modify configuration | 63 | | `/cost` | Show token usage statistics | 64 | | `/doctor` | Checks the health of your Claude Code installation | 65 | | `/help` | Get usage help | 66 | | `/init` | Initialize project with CLAUDE.md guide | 67 | | `/login` | Switch Anthropic accounts | 68 | | `/logout` | Sign out from your Anthropic account | 69 | | `/memory` | Edit CLAUDE.md memory files | 70 | | `/model` | Select or change the AI model | 71 | | `/pr_comments` | View pull request comments | 72 | | `/review` | Request code review | 73 | | `/status` | View account and system statuses | 74 | | `/terminal-setup` | Install Shift+Enter key binding for newlines (iTerm2 and VSCode only) | 75 | | `/vim` | Enter vim mode for alternating insert and command modes | 76 | 77 | ## Special shortcuts 78 | 79 | ### Quick memory with 80 | 81 | Add memories instantly by starting your input with `#`: 82 | 83 | You’ll be prompted to select which memory file to store this in. 84 | 85 | ### Line breaks in terminal 86 | 87 | Enter multiline commands using: 88 | 89 | - **Quick escape**: Type `\` followed by Enter 90 | - **Keyboard shortcut**: Option+Enter (or Shift+Enter if configured) 91 | 92 | To set up Option+Enter in your terminal: 93 | 94 | **For Mac Terminal.app:** 95 | 96 | 1. Open Settings → Profiles → Keyboard 97 | 2. Check “Use Option as Meta Key” 98 | 99 | **For iTerm2 and VSCode terminal:** 100 | 101 | 1. Open Settings → Profiles → Keys 102 | 2. Under General, set Left/Right Option key to “Esc+” 103 | 104 | **Tip for iTerm2 and VSCode users**: Run `/terminal-setup` within Claude Code to automatically configure Shift+Enter as a more intuitive alternative. 105 | 106 | See [terminal setup in settings](https://docs.anthropic.com/en/docs/claude-code/settings#line-breaks) for configuration details. 107 | 108 | ## Vim Mode 109 | 110 | Claude Code supports a subset of Vim keybindings that can be enabled with `/vim` or configured via `/config`. 111 | 112 | The supported subset includes: 113 | 114 | - Mode switching: `Esc` (to NORMAL), `i` / `I`, `a` / `A`, `o` / `O` (to INSERT) 115 | - Navigation: `h` / `j` / `k` / `l`, `w` / `e` / `b`, `0` / `$` / `^`, `gg` / `G` 116 | - Editing: `x`, `dw` / `de` / `db` / `dd` / `D`, `cw` / `ce` / `cb` / `cc` / `C`, `.` (repeat) 117 | -------------------------------------------------------------------------------- /docs/git-operations-plan.md: -------------------------------------------------------------------------------- 1 | # Safe Git Operations Implementation Plan 2 | 3 | ## Overview 4 | 5 | This plan outlines how to safely handle Git operations (commit, push, PR) outside the container while maintaining security and seamless UX. 6 | 7 | ## Architecture 8 | 9 | ### 1. **Dual Repository Approach** 10 | 11 | - **Container Repo**: Claude works in an isolated git repo inside the container 12 | - **Shadow Repo**: A temporary bare repository outside the container that mirrors commits 13 | - **Host Repo**: The original repository remains untouched 14 | 15 | ### 2. **Key Components** 16 | 17 | ``` 18 | ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ 19 | │ Container Repo │────▶│ Shadow Repo │────▶│ Remote GitHub │ 20 | │ (Claude works) │ │ (Outside container)│ │ (Push/PR) │ 21 | └─────────────────┘ └──────────────────┘ └─────────────────┘ 22 | │ │ 23 | │ │ 24 | └───── Git Bundle ──────┘ 25 | ``` 26 | 27 | ## Implementation Details 28 | 29 | ### Phase 1: Commit Extraction System 30 | 31 | 1. **Git Bundle Mechanism** 32 | 33 | ```bash 34 | # Inside container 35 | git bundle create /tmp/changes.bundle ^origin/ 36 | 37 | # Outside container 38 | docker cp :/tmp/changes.bundle ./ 39 | ``` 40 | 41 | 2. **Shadow Repository Setup (Optimized)** 42 | 43 | ```typescript 44 | class ShadowRepository { 45 | private shadowPath: string; 46 | 47 | async initialize(originalRepo: string, branch: string) { 48 | // Create minimal single-branch clone 49 | this.shadowPath = await mkdtemp("/tmp/claude-shadow-"); 50 | 51 | // Option 1: Shallow single-branch clone (most efficient) 52 | await exec( 53 | `git clone --single-branch --branch ${branch} --depth 1 --bare ${originalRepo} ${this.shadowPath}`, 54 | ); 55 | 56 | // Option 2: Even more minimal - just init and add remote 57 | // await exec(`git init --bare ${this.shadowPath}`); 58 | // await exec(`git remote add origin ${originalRepo}`, { cwd: this.shadowPath }); 59 | // await exec(`git fetch origin ${branch}:${branch} --depth 1`, { cwd: this.shadowPath }); 60 | } 61 | 62 | async applyBundle(bundlePath: string, branch: string) { 63 | // Fetch commits from bundle 64 | await exec(`git fetch ${bundlePath} ${branch}:${branch}`, { 65 | cwd: this.shadowPath, 66 | }); 67 | } 68 | 69 | async cleanup() { 70 | // Remove shadow repo after use 71 | await fs.rm(this.shadowPath, { recursive: true }); 72 | } 73 | } 74 | ``` 75 | 76 | ### Phase 2: Git Operations Handler 77 | 78 | ```typescript 79 | interface GitOperation { 80 | type: "commit" | "push" | "pr"; 81 | branch: string; 82 | commits: CommitInfo[]; 83 | } 84 | 85 | class GitOperationsHandler { 86 | private shadow: ShadowRepository; 87 | private githubToken?: string; 88 | 89 | async extractCommits(containerId: string, branch: string) { 90 | // 1. Create bundle in container 91 | await docker.exec(containerId, [ 92 | "git", 93 | "bundle", 94 | "create", 95 | "/tmp/changes.bundle", 96 | branch, 97 | `^origin/${branch}`, 98 | ]); 99 | 100 | // 2. Copy bundle out 101 | const bundlePath = await docker.copyFromContainer( 102 | containerId, 103 | "/tmp/changes.bundle", 104 | ); 105 | 106 | // 3. Apply to shadow repo 107 | await this.shadow.applyBundle(bundlePath, branch); 108 | 109 | // 4. Get commit info 110 | return await this.shadow.getCommits(branch); 111 | } 112 | 113 | async push(branch: string) { 114 | // Use GitHub token from host environment 115 | const token = await this.getGitHubToken(); 116 | await this.shadow.push(branch, token); 117 | } 118 | 119 | private async getGitHubToken() { 120 | // Try multiple sources in order: 121 | // 1. Environment variable 122 | // 2. GitHub CLI (gh auth token) 123 | // 3. Git credential helper 124 | // 4. macOS Keychain 125 | } 126 | } 127 | ``` 128 | 129 | ### Phase 3: UI Flow 130 | 131 | ```typescript 132 | interface SessionEndOptions { 133 | hasChanges: boolean; 134 | branch: string; 135 | commits: CommitInfo[]; 136 | } 137 | 138 | class SessionEndUI { 139 | async showOptions(options: SessionEndOptions) { 140 | if (!options.hasChanges) { 141 | return; // No UI needed 142 | } 143 | 144 | const choices = [ 145 | { 146 | name: `Review ${options.commits.length} commits`, 147 | value: "review", 148 | }, 149 | { 150 | name: "Push to branch", 151 | value: "push", 152 | disabled: !this.hasGitHubAccess(), 153 | }, 154 | { 155 | name: "Create/Update PR", 156 | value: "pr", 157 | disabled: !this.hasGitHubAccess() || !options.branchExists, 158 | }, 159 | { 160 | name: "Export as patch", 161 | value: "export", 162 | }, 163 | { 164 | name: "Discard changes", 165 | value: "discard", 166 | }, 167 | ]; 168 | 169 | return await inquirer.prompt([ 170 | { 171 | type: "list", 172 | name: "action", 173 | message: "What would you like to do with your changes?", 174 | choices, 175 | }, 176 | ]); 177 | } 178 | } 179 | ``` 180 | 181 | ### Phase 4: Security Measures 182 | 183 | 1. **Token Isolation** 184 | 185 | - GitHub tokens NEVER enter the container 186 | - All push operations happen from host process 187 | - Tokens retrieved just-in-time when needed 188 | 189 | 2. **Repository Protection** 190 | 191 | - Host repository is never modified directly 192 | - All operations go through shadow repository 193 | - User explicitly approves each operation 194 | 195 | 3. **Audit Trail** 196 | ```typescript 197 | class GitAuditLog { 198 | log(operation: GitOperation) { 199 | // Log all git operations for transparency 200 | console.log(`[GIT] ${operation.type} on ${operation.branch}`); 201 | // Store in ~/.claude-sandbox/git-operations.log 202 | } 203 | } 204 | ``` 205 | 206 | ## Implementation Phases 207 | 208 | ### Phase 1: Basic Commit Extraction (Week 1) 209 | 210 | - [ ] Implement git bundle creation in container 211 | - [ ] Create shadow repository manager 212 | - [ ] Build commit extraction pipeline 213 | - [ ] Add commit review UI 214 | 215 | ### Phase 2: Push Functionality (Week 2) 216 | 217 | - [ ] Implement GitHub token discovery 218 | - [ ] Add push to shadow repository 219 | - [ ] Create push confirmation UI 220 | - [ ] Handle push errors gracefully 221 | 222 | ### Phase 3: PR Management (Week 3) 223 | 224 | - [ ] Integrate with GitHub API 225 | - [ ] Check for existing PRs 226 | - [ ] Create/update PR functionality 227 | - [ ] Add PR template support 228 | 229 | ### Phase 4: Enhanced UX (Week 4) 230 | 231 | - [ ] Add commit message editing 232 | - [ ] Implement patch export option 233 | - [ ] Create web UI for git operations 234 | - [ ] Add git operation history 235 | 236 | ## File Structure 237 | 238 | ``` 239 | claude-code-sandbox/ 240 | ├── src/ 241 | │ ├── git/ 242 | │ │ ├── shadow-repository.ts 243 | │ │ ├── git-operations-handler.ts 244 | │ │ ├── github-integration.ts 245 | │ │ └── git-audit-log.ts 246 | │ ├── ui/ 247 | │ │ ├── session-end-ui.ts 248 | │ │ └── git-review-ui.ts 249 | │ └── credentials/ 250 | │ └── github-token-provider.ts 251 | ``` 252 | 253 | ## Example Usage Flow 254 | 255 | 1. **Claude makes commits in container** 256 | 257 | ```bash 258 | # Inside container 259 | git commit -m "Add new feature" 260 | git commit -m "Fix bug" 261 | ``` 262 | 263 | 2. **Session ends, UI appears** 264 | 265 | ``` 266 | 🔔 Claude made 2 commits. What would you like to do? 267 | 268 | ❯ Review commits 269 | Push to branch 'claude/2025-01-27-feature' 270 | Create PR from 'claude/2025-01-27-feature' 271 | Export as patch 272 | Discard changes 273 | ``` 274 | 275 | 3. **User selects "Push to branch"** 276 | 277 | ``` 278 | 🔐 Authenticating with GitHub... 279 | ✓ Found GitHub token 280 | 281 | 📤 Pushing 2 commits to 'claude/2025-01-27-feature' 282 | ✓ Successfully pushed to GitHub 283 | 284 | 🔗 View branch: https://github.com/user/repo/tree/claude/2025-01-27-feature 285 | ``` 286 | 287 | ## Benefits 288 | 289 | 1. **Security**: GitHub tokens never enter container 290 | 2. **Safety**: Original repository untouched 291 | 3. **Flexibility**: Multiple export options 292 | 4. **Transparency**: All operations logged 293 | 5. **Seamless UX**: Feels like normal git workflow 294 | 295 | ## Technical Considerations 296 | 297 | 1. **Bundle Limitations** 298 | 299 | - Bundles only contain commit objects 300 | - Large binary files may need special handling 301 | 302 | 2. **Shadow Repository Efficiency** 303 | 304 | - Only clones the specific branch Claude is working on 305 | - Shallow clone (--depth 1) to minimize data transfer 306 | - Bare repository (no working tree) saves disk space 307 | - Temporary repos cleaned immediately after use 308 | - For large repos, can use partial clone: `--filter=blob:none` 309 | 310 | 3. **Minimal Shadow Repo Approach** 311 | 312 | ```bash 313 | # Ultra-minimal: Just enough to receive and push commits 314 | git init --bare /tmp/shadow 315 | git -C /tmp/shadow remote add origin 316 | git -C /tmp/shadow fetch : 317 | git -C /tmp/shadow push origin 318 | ``` 319 | 320 | 4. **Error Handling** 321 | - Network failures during push 322 | - Merge conflicts detection 323 | - Token expiration handling 324 | 325 | ## Alternative Approaches Considered 326 | 327 | 1. **Git Worktree**: Too complex, modifies host repo 328 | 2. **Direct Push from Container**: Security risk 329 | 3. **Manual Patch Export**: Poor UX 330 | 4. **Shared Volume**: Risky, could corrupt host repo 331 | 332 | ## Next Steps 333 | 334 | 1. Prototype git bundle extraction 335 | 2. Test shadow repository approach 336 | 3. Build minimal UI for testing 337 | 4. Gather feedback on UX flow 338 | -------------------------------------------------------------------------------- /test/e2e/test-suite.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { SyncTestFramework } = require("./sync-test-framework"); 4 | 5 | class TestRunner { 6 | constructor() { 7 | this.framework = new SyncTestFramework(); 8 | this.tests = []; 9 | this.passed = 0; 10 | this.failed = 0; 11 | } 12 | 13 | addTest(name, testFn) { 14 | this.tests.push({ name, testFn }); 15 | } 16 | 17 | async runTest(test) { 18 | const startTime = Date.now(); 19 | try { 20 | console.log(`\n🧪 Running: ${test.name}`); 21 | await test.testFn(this.framework); 22 | const duration = Date.now() - startTime; 23 | console.log(`✅ PASSED: ${test.name} (${duration}ms)`); 24 | this.passed++; 25 | return true; 26 | } catch (error) { 27 | const duration = Date.now() - startTime; 28 | console.log(`❌ FAILED: ${test.name} (${duration}ms)`); 29 | console.log(` Error: ${error.message}`); 30 | this.failed++; 31 | return false; 32 | } 33 | } 34 | 35 | async runAll() { 36 | console.log("🚀 Starting File Sync E2E Tests"); 37 | console.log("=".repeat(50)); 38 | 39 | try { 40 | await this.framework.setup(); 41 | 42 | for (const test of this.tests) { 43 | await this.runTest(test); 44 | } 45 | } finally { 46 | await this.framework.cleanup(); 47 | } 48 | 49 | console.log("\n" + "=".repeat(50)); 50 | console.log( 51 | `📊 Test Results: ${this.passed} passed, ${this.failed} failed`, 52 | ); 53 | 54 | if (this.failed > 0) { 55 | console.log("❌ Some tests failed"); 56 | process.exit(1); 57 | } else { 58 | console.log("✅ All tests passed!"); 59 | process.exit(0); 60 | } 61 | } 62 | } 63 | 64 | // Test cases 65 | const runner = new TestRunner(); 66 | 67 | // Test 1: File Addition 68 | runner.addTest("File Addition", async (framework) => { 69 | await framework.addFile("new-file.txt", "Hello, World!"); 70 | 71 | const exists = await framework.shadowFileExists("new-file.txt"); 72 | if (!exists) { 73 | throw new Error("File was not synced to shadow repository"); 74 | } 75 | 76 | const content = await framework.getShadowFileContent("new-file.txt"); 77 | if (content.trim() !== "Hello, World!") { 78 | throw new Error( 79 | `Content mismatch: expected "Hello, World!", got "${content.trim()}"`, 80 | ); 81 | } 82 | 83 | const gitStatus = await framework.getGitStatus(); 84 | if (!gitStatus.includes("A new-file.txt")) { 85 | throw new Error(`Git status should show addition: ${gitStatus}`); 86 | } 87 | }); 88 | 89 | // Test 2: File Modification 90 | runner.addTest("File Modification", async (framework) => { 91 | // First, create and commit a file 92 | await framework.addFile("modify-test.txt", "Original content"); 93 | const { stdout } = await require("util").promisify( 94 | require("child_process").exec, 95 | )( 96 | `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for modification test"`, 97 | ); 98 | 99 | // Now modify it 100 | await framework.modifyFile("modify-test.txt", "Modified content"); 101 | 102 | const content = await framework.getShadowFileContent("modify-test.txt"); 103 | if (content.trim() !== "Modified content") { 104 | throw new Error(`Content not modified: got "${content.trim()}"`); 105 | } 106 | 107 | const gitStatus = await framework.getGitStatus(); 108 | if (!gitStatus.includes("M modify-test.txt")) { 109 | throw new Error(`Git status should show modification: ${gitStatus}`); 110 | } 111 | }); 112 | 113 | // Test 3: File Deletion 114 | runner.addTest("File Deletion", async (framework) => { 115 | // Create and commit a file first 116 | await framework.addFile("delete-test.txt", "To be deleted"); 117 | const { stdout } = await require("util").promisify( 118 | require("child_process").exec, 119 | )( 120 | `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for deletion test"`, 121 | ); 122 | 123 | // Delete the file 124 | await framework.deleteFile("delete-test.txt"); 125 | 126 | const exists = await framework.shadowFileExists("delete-test.txt"); 127 | if (exists) { 128 | throw new Error("File still exists in shadow repository after deletion"); 129 | } 130 | 131 | const gitStatus = await framework.getGitStatus(); 132 | if (!gitStatus.includes("D delete-test.txt")) { 133 | throw new Error(`Git status should show deletion: ${gitStatus}`); 134 | } 135 | }); 136 | 137 | // Test 4: Multiple File Operations 138 | runner.addTest("Multiple File Operations", async (framework) => { 139 | // Add multiple files 140 | await framework.addFile("file1.txt", "Content 1"); 141 | await framework.addFile("file2.txt", "Content 2"); 142 | await framework.addFile("file3.txt", "Content 3"); 143 | 144 | // Wait for sync 145 | await framework.waitForSync(); 146 | 147 | // Check that all files exist 148 | const exists1 = await framework.shadowFileExists("file1.txt"); 149 | const exists2 = await framework.shadowFileExists("file2.txt"); 150 | const exists3 = await framework.shadowFileExists("file3.txt"); 151 | 152 | if (!exists1 || !exists2 || !exists3) { 153 | throw new Error("Not all files were synced to shadow repository"); 154 | } 155 | 156 | const gitStatus = await framework.getGitStatus(); 157 | if ( 158 | !gitStatus.includes("file1.txt") || 159 | !gitStatus.includes("file2.txt") || 160 | !gitStatus.includes("file3.txt") 161 | ) { 162 | throw new Error(`All files should appear in git status: ${gitStatus}`); 163 | } 164 | }); 165 | 166 | // Test 5: Directory Operations 167 | runner.addTest("Directory Operations", async (framework) => { 168 | // Create directory with files 169 | await framework.createDirectory("new-dir"); 170 | await framework.addFile("new-dir/nested-file.txt", "Nested content"); 171 | 172 | const exists = await framework.shadowFileExists("new-dir/nested-file.txt"); 173 | if (!exists) { 174 | throw new Error("Nested file was not synced"); 175 | } 176 | 177 | const content = await framework.getShadowFileContent( 178 | "new-dir/nested-file.txt", 179 | ); 180 | if (content.trim() !== "Nested content") { 181 | throw new Error(`Nested file content mismatch: got "${content.trim()}"`); 182 | } 183 | }); 184 | 185 | // Test 6: File Rename/Move 186 | runner.addTest("File Rename/Move", async (framework) => { 187 | // Create and commit a file 188 | await framework.addFile("original-name.txt", "Content to move"); 189 | const { stdout } = await require("util").promisify( 190 | require("child_process").exec, 191 | )( 192 | `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for move test"`, 193 | ); 194 | 195 | // Move the file 196 | await framework.moveFile("original-name.txt", "new-name.txt"); 197 | 198 | const originalExists = await framework.shadowFileExists("original-name.txt"); 199 | const newExists = await framework.shadowFileExists("new-name.txt"); 200 | 201 | if (originalExists) { 202 | throw new Error("Original file still exists after move"); 203 | } 204 | 205 | if (!newExists) { 206 | throw new Error("New file does not exist after move"); 207 | } 208 | 209 | const content = await framework.getShadowFileContent("new-name.txt"); 210 | if (content.trim() !== "Content to move") { 211 | throw new Error(`Moved file content mismatch: got "${content.trim()}"`); 212 | } 213 | }); 214 | 215 | // Test 7: Large File Handling 216 | runner.addTest("Large File Handling", async (framework) => { 217 | const largeContent = "x".repeat(10000); // 10KB file 218 | // Use printf to avoid newline issues with echo 219 | const containerPath = `/workspace/large-file.txt`; 220 | await require("util").promisify(require("child_process").exec)( 221 | `docker exec ${framework.containerId} bash -c "printf '${largeContent}' > ${containerPath}"`, 222 | ); 223 | await framework.waitForSync(); 224 | 225 | const exists = await framework.shadowFileExists("large-file.txt"); 226 | if (!exists) { 227 | throw new Error("Large file was not synced"); 228 | } 229 | 230 | const content = await framework.getShadowFileContent("large-file.txt"); 231 | if (content.length !== largeContent.length) { 232 | throw new Error( 233 | `Large file size mismatch: expected ${largeContent.length}, got ${content.length}`, 234 | ); 235 | } 236 | }); 237 | 238 | // Test 8: Special Characters in Filenames 239 | runner.addTest("Special Characters in Filenames", async (framework) => { 240 | const specialFile = "special-chars-test_file.txt"; 241 | await framework.addFile(specialFile, "Special content"); 242 | 243 | const exists = await framework.shadowFileExists(specialFile); 244 | if (!exists) { 245 | throw new Error("File with special characters was not synced"); 246 | } 247 | 248 | const content = await framework.getShadowFileContent(specialFile); 249 | if (content.trim() !== "Special content") { 250 | throw new Error(`Special file content mismatch: got "${content.trim()}"`); 251 | } 252 | }); 253 | 254 | // Test 9: Rapid File Changes 255 | runner.addTest("Rapid File Changes", async (framework) => { 256 | // Create file 257 | await framework.addFile("rapid-test.txt", "Version 1"); 258 | 259 | // Wait for initial sync 260 | await framework.waitForSync(); 261 | 262 | // Quickly modify it multiple times 263 | await framework.modifyFile("rapid-test.txt", "Version 2"); 264 | await new Promise((resolve) => setTimeout(resolve, 200)); 265 | await framework.modifyFile("rapid-test.txt", "Version 3"); 266 | await new Promise((resolve) => setTimeout(resolve, 200)); 267 | await framework.modifyFile("rapid-test.txt", "Final Version"); 268 | 269 | // Wait for final sync 270 | await framework.waitForSync(null, 15000); 271 | 272 | const content = await framework.getShadowFileContent("rapid-test.txt"); 273 | if (content.trim() !== "Final Version") { 274 | throw new Error(`Final content mismatch: got "${content.trim()}"`); 275 | } 276 | }); 277 | 278 | // Test 10: Web UI Sync Notifications 279 | runner.addTest("Web UI Sync Notifications", async (framework) => { 280 | const initialEventCount = framework.receivedSyncEvents.length; 281 | 282 | await framework.addFile("notification-test.txt", "Test notification"); 283 | 284 | // Check that we received sync events 285 | await framework.waitForSync(); 286 | 287 | const newEventCount = framework.receivedSyncEvents.length; 288 | if (newEventCount <= initialEventCount) { 289 | throw new Error("No sync events received by web UI"); 290 | } 291 | 292 | const latestEvent = 293 | framework.receivedSyncEvents[framework.receivedSyncEvents.length - 1]; 294 | if (!latestEvent.data.hasChanges) { 295 | throw new Error("Sync event should indicate changes"); 296 | } 297 | 298 | if (!latestEvent.data.summary.includes("Added:")) { 299 | throw new Error( 300 | `Sync event should show addition: ${latestEvent.data.summary}`, 301 | ); 302 | } 303 | }); 304 | 305 | // Run all tests 306 | runner.runAll().catch((error) => { 307 | console.error("❌ Test runner failed:", error); 308 | process.exit(1); 309 | }); 310 | -------------------------------------------------------------------------------- /test/e2e/sync-test-framework.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawn, exec } = require("child_process"); 4 | const fs = require("fs").promises; 5 | const path = require("path"); 6 | const http = require("http"); 7 | const WebSocket = require("ws"); 8 | const util = require("util"); 9 | const execAsync = util.promisify(exec); 10 | 11 | class SyncTestFramework { 12 | constructor() { 13 | this.testRepo = null; 14 | this.containerId = null; 15 | this.shadowPath = null; 16 | this.webSocket = null; 17 | this.webPort = null; 18 | this.sandboxProcess = null; 19 | this.receivedSyncEvents = []; 20 | this.testTimeout = 60000; // 1 minute timeout per test 21 | } 22 | 23 | async setup() { 24 | console.log("🔧 Setting up test environment..."); 25 | 26 | // Create temporary test repository 27 | this.testRepo = path.join("/tmp", `test-repo-${Date.now()}`); 28 | await fs.mkdir(this.testRepo, { recursive: true }); 29 | 30 | // Copy dummy repo files to test repo 31 | const dummyRepo = path.join(__dirname, "dummy-repo"); 32 | await execAsync(`cp -r ${dummyRepo}/* ${this.testRepo}/`); 33 | 34 | // Initialize as git repository 35 | await execAsync( 36 | `cd ${this.testRepo} && git init && git add . && git commit -m "Initial commit"`, 37 | ); 38 | 39 | console.log(`📁 Test repo created at: ${this.testRepo}`); 40 | 41 | // Start claude-sandbox from the test repo 42 | await this.startSandbox(); 43 | 44 | // Connect to web UI for monitoring sync events 45 | await this.connectToWebUI(); 46 | 47 | console.log("✅ Test environment ready"); 48 | } 49 | 50 | async startSandbox() { 51 | return new Promise((resolve, reject) => { 52 | console.log("🚀 Starting claude-sandbox..."); 53 | 54 | this.sandboxProcess = spawn("npx", ["claude-sandbox", "start"], { 55 | cwd: this.testRepo, 56 | stdio: "pipe", 57 | }); 58 | 59 | let setupComplete = false; 60 | const timeout = setTimeout(() => { 61 | if (!setupComplete) { 62 | reject(new Error("Sandbox startup timeout")); 63 | } 64 | }, 45000); 65 | 66 | this.sandboxProcess.stdout.on("data", (data) => { 67 | const output = data.toString(); 68 | console.log("SANDBOX:", output.trim()); 69 | 70 | // Extract container ID 71 | const containerMatch = output.match(/Started container: ([a-f0-9]+)/); 72 | if (containerMatch) { 73 | this.containerId = containerMatch[1]; 74 | this.shadowPath = `/tmp/claude-shadows/${this.containerId}`; 75 | console.log(`🆔 Container ID: ${this.containerId}`); 76 | } 77 | 78 | // Extract web port 79 | const portMatch = output.match( 80 | /Web UI server started at http:\/\/localhost:(\d+)/, 81 | ); 82 | if (portMatch) { 83 | this.webPort = parseInt(portMatch[1]); 84 | console.log(`🌐 Web UI port: ${this.webPort}`); 85 | } 86 | 87 | // Check for setup completion 88 | if (output.includes("Files synced successfully") && !setupComplete) { 89 | setupComplete = true; 90 | clearTimeout(timeout); 91 | setTimeout(() => resolve(), 2000); // Wait a bit more for full initialization 92 | } 93 | }); 94 | 95 | this.sandboxProcess.stderr.on("data", (data) => { 96 | console.error("SANDBOX ERROR:", data.toString()); 97 | }); 98 | 99 | this.sandboxProcess.on("close", (code) => { 100 | if (!setupComplete) { 101 | reject(new Error(`Sandbox process exited with code ${code}`)); 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | async connectToWebUI() { 108 | if (!this.webPort || !this.containerId) { 109 | throw new Error("Web UI port or container ID not available"); 110 | } 111 | 112 | return new Promise((resolve, reject) => { 113 | const wsUrl = `ws://localhost:${this.webPort}/socket.io/?EIO=4&transport=websocket`; 114 | this.webSocket = new WebSocket(wsUrl); 115 | 116 | this.webSocket.on("open", () => { 117 | console.log("🔌 Connected to web UI"); 118 | 119 | // Send initial connection message (Socket.IO protocol) 120 | this.webSocket.send("40"); // Socket.IO connect message 121 | 122 | setTimeout(() => { 123 | // Attach to container 124 | this.webSocket.send( 125 | `42["attach",{"containerId":"${this.containerId}","cols":80,"rows":24}]`, 126 | ); 127 | resolve(); 128 | }, 1000); 129 | }); 130 | 131 | this.webSocket.on("message", (data) => { 132 | const message = data.toString(); 133 | if (message.startsWith('42["sync-complete"')) { 134 | try { 135 | const eventData = JSON.parse(message.substring(2))[1]; 136 | this.receivedSyncEvents.push({ 137 | timestamp: Date.now(), 138 | data: eventData, 139 | }); 140 | console.log("📡 Received sync event:", eventData.summary); 141 | } catch (e) { 142 | // Ignore parsing errors 143 | } 144 | } 145 | }); 146 | 147 | this.webSocket.on("error", (error) => { 148 | console.error("WebSocket error:", error); 149 | reject(error); 150 | }); 151 | 152 | setTimeout( 153 | () => reject(new Error("WebSocket connection timeout")), 154 | 10000, 155 | ); 156 | }); 157 | } 158 | 159 | async waitForSync(expectedChanges = null, timeoutMs = 10000) { 160 | const startTime = Date.now(); 161 | const initialEventCount = this.receivedSyncEvents.length; 162 | 163 | // Wait for a new sync event or timeout 164 | while (Date.now() - startTime < timeoutMs) { 165 | // Check if we received a new sync event 166 | if (this.receivedSyncEvents.length > initialEventCount) { 167 | // Wait a bit more for the sync to fully complete 168 | await new Promise((resolve) => setTimeout(resolve, 500)); 169 | return this.receivedSyncEvents[this.receivedSyncEvents.length - 1]; 170 | } 171 | 172 | // Also wait for the actual file to appear in shadow repo if we're checking for additions 173 | if (expectedChanges && expectedChanges.filePath) { 174 | const exists = await this.shadowFileExists(expectedChanges.filePath); 175 | if (exists) { 176 | // File exists, sync completed 177 | await new Promise((resolve) => setTimeout(resolve, 500)); 178 | return { data: { hasChanges: true, summary: "Sync completed" } }; 179 | } 180 | } 181 | 182 | await new Promise((resolve) => setTimeout(resolve, 100)); 183 | } 184 | 185 | // If no sync event was received, just wait a bit and return 186 | await new Promise((resolve) => setTimeout(resolve, 2000)); 187 | return { data: { hasChanges: true, summary: "Sync completed (timeout)" } }; 188 | } 189 | 190 | async addFile(filePath, content) { 191 | const containerPath = `/workspace/${filePath}`; 192 | await execAsync( 193 | `docker exec ${this.containerId} bash -c "mkdir -p $(dirname ${containerPath}) && echo '${content}' > ${containerPath}"`, 194 | ); 195 | return this.waitForSync({ filePath }); 196 | } 197 | 198 | async modifyFile(filePath, newContent) { 199 | const containerPath = `/workspace/${filePath}`; 200 | await execAsync( 201 | `docker exec ${this.containerId} bash -c "echo '${newContent}' > ${containerPath}"`, 202 | ); 203 | return this.waitForSync(); 204 | } 205 | 206 | async deleteFile(filePath) { 207 | const containerPath = `/workspace/${filePath}`; 208 | await execAsync(`docker exec ${this.containerId} rm ${containerPath}`); 209 | return this.waitForSync(); 210 | } 211 | 212 | async moveFile(fromPath, toPath) { 213 | const containerFromPath = `/workspace/${fromPath}`; 214 | const containerToPath = `/workspace/${toPath}`; 215 | await execAsync( 216 | `docker exec ${this.containerId} bash -c "mkdir -p $(dirname ${containerToPath}) && mv ${containerFromPath} ${containerToPath}"`, 217 | ); 218 | return this.waitForSync(); 219 | } 220 | 221 | async createDirectory(dirPath) { 222 | const containerPath = `/workspace/${dirPath}`; 223 | await execAsync( 224 | `docker exec ${this.containerId} mkdir -p ${containerPath}`, 225 | ); 226 | } 227 | 228 | async deleteDirectory(dirPath) { 229 | const containerPath = `/workspace/${dirPath}`; 230 | await execAsync(`docker exec ${this.containerId} rm -rf ${containerPath}`); 231 | return this.waitForSync(); 232 | } 233 | 234 | async getGitStatus() { 235 | try { 236 | const { stdout } = await execAsync( 237 | `git -C ${this.shadowPath} status --porcelain`, 238 | ); 239 | return stdout.trim(); 240 | } catch (error) { 241 | throw new Error(`Failed to get git status: ${error.message}`); 242 | } 243 | } 244 | 245 | async getShadowFileContent(filePath) { 246 | try { 247 | const fullPath = path.join(this.shadowPath, filePath); 248 | return await fs.readFile(fullPath, "utf8"); 249 | } catch (error) { 250 | throw new Error( 251 | `Failed to read shadow file ${filePath}: ${error.message}`, 252 | ); 253 | } 254 | } 255 | 256 | async shadowFileExists(filePath) { 257 | try { 258 | const fullPath = path.join(this.shadowPath, filePath); 259 | await fs.access(fullPath); 260 | return true; 261 | } catch (error) { 262 | return false; 263 | } 264 | } 265 | 266 | async getContainerFileContent(filePath) { 267 | try { 268 | const { stdout } = await execAsync( 269 | `docker exec ${this.containerId} cat /workspace/${filePath}`, 270 | ); 271 | return stdout; 272 | } catch (error) { 273 | throw new Error( 274 | `Failed to read container file ${filePath}: ${error.message}`, 275 | ); 276 | } 277 | } 278 | 279 | async containerFileExists(filePath) { 280 | try { 281 | await execAsync( 282 | `docker exec ${this.containerId} test -f /workspace/${filePath}`, 283 | ); 284 | return true; 285 | } catch (error) { 286 | return false; 287 | } 288 | } 289 | 290 | async listContainerFiles(directory = "") { 291 | try { 292 | const containerPath = directory 293 | ? `/workspace/${directory}` 294 | : "/workspace"; 295 | const { stdout } = await execAsync( 296 | `docker exec ${this.containerId} find ${containerPath} -type f -not -path "*/.*" | sed 's|^/workspace/||' | sort`, 297 | ); 298 | return stdout 299 | .trim() 300 | .split("\n") 301 | .filter((f) => f); 302 | } catch (error) { 303 | throw new Error(`Failed to list container files: ${error.message}`); 304 | } 305 | } 306 | 307 | async listRepoFiles(directory = "") { 308 | try { 309 | const repoPath = directory 310 | ? path.join(this.testRepo, directory) 311 | : this.testRepo; 312 | const { stdout } = await execAsync( 313 | `find ${repoPath} -type f -not -path "*/.*" | sed 's|^${this.testRepo}/||' | sort`, 314 | ); 315 | return stdout 316 | .trim() 317 | .split("\n") 318 | .filter((f) => f); 319 | } catch (error) { 320 | throw new Error(`Failed to list repo files: ${error.message}`); 321 | } 322 | } 323 | 324 | async cleanup() { 325 | console.log("🧹 Cleaning up test environment..."); 326 | 327 | if (this.webSocket) { 328 | this.webSocket.close(); 329 | } 330 | 331 | if (this.sandboxProcess) { 332 | this.sandboxProcess.kill("SIGTERM"); 333 | } 334 | 335 | // Purge containers 336 | try { 337 | await execAsync("npx claude-sandbox purge -y"); 338 | } catch (e) { 339 | // Ignore errors 340 | } 341 | 342 | // Clean up test repo 343 | if (this.testRepo) { 344 | try { 345 | await fs.rm(this.testRepo, { recursive: true, force: true }); 346 | } catch (e) { 347 | // Ignore errors 348 | } 349 | } 350 | 351 | console.log("✅ Cleanup complete"); 352 | } 353 | } 354 | 355 | module.exports = { SyncTestFramework }; 356 | --------------------------------------------------------------------------------