Skip to content

Commit c79b197

Browse files
feat: add configurable working directory with isolated default (#14)
- Default to temporary directory for security isolation - Validate directory existence before use (fail gracefully) - Support CLAUDE_CWD environment variable for custom directories - Add comprehensive cross-platform tests - Update Docker configuration for workspace mounting - Add clear documentation for all configuration options This prevents Claude Code from accessing the wrapper's source code by default, enhancing security while maintaining flexibility. Co-authored-by: George Elphick <george.elphick@talieisin.co.uk>
1 parent c2d9f97 commit c79b197

5 files changed

Lines changed: 351 additions & 15 deletions

File tree

README.md

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,40 @@ MAX_TIMEOUT=600000
143143
144144
# CORS origins
145145
CORS_ORIGINS=["*"]
146+
147+
# Working directory for Claude Code (optional)
148+
# If not set, uses an isolated temporary directory for security
149+
# CLAUDE_CWD=/path/to/your/workspace
146150
```
147151

152+
### 📁 **Working Directory Configuration**
153+
154+
By default, Claude Code runs in an **isolated temporary directory** to prevent it from accessing the wrapper's source code. This enhances security by ensuring Claude Code only has access to the workspace you intend.
155+
156+
**Configuration Options:**
157+
158+
1. **Default (Recommended)**: Automatically creates a temporary isolated workspace
159+
```bash
160+
# No configuration needed - secure by default
161+
poetry run python main.py
162+
```
163+
164+
2. **Custom Directory**: Set a specific workspace directory
165+
```bash
166+
export CLAUDE_CWD=/path/to/your/project
167+
poetry run python main.py
168+
```
169+
170+
3. **Via .env file**: Add to your `.env` file
171+
```env
172+
CLAUDE_CWD=/home/user/my-workspace
173+
```
174+
175+
**Important Notes:**
176+
- The temporary directory is automatically cleaned up when the server stops
177+
- This prevents Claude Code from accidentally modifying the wrapper's own code
178+
- Cross-platform compatible (Windows, macOS, Linux)
179+
148180
### 🔐 **API Security Configuration**
149181

150182
The server supports **interactive API key protection** for secure remote access:
@@ -246,7 +278,7 @@ RATE_LIMIT_HEALTH_PER_MINUTE=30
246278

247279
This guide provides a comprehensive overview of building, running, and configuring a Docker container for the Claude Code OpenAI Wrapper. Docker enables isolated, portable, and reproducible deployments of the wrapper, which acts as an OpenAI-compatible API server routing requests to Anthropic's Claude models via the official Claude Code Python SDK (v0.0.14+). This setup supports authentication methods like Claude subscriptions (e.g., Max plan via OAuth for fixed-cost quotas), direct API keys, AWS Bedrock, or Google Vertex AI.
248280
249-
By containerizing the application, you can run it locally for development, deploy it to remote servers or cloud platforms, and customize behavior through environment variables and volumes. This guide assumes you have already cloned the repository and have the `Dockerfile` in the root directory. For general repository setup (e.g., Claude Code CLI authentication), refer to the sections above.
281+
By containerizing the application, you can run it locally for development, deploy it to remote servers or cloud platforms, and customise behaviour through environment variables and volumes. This guide assumes you have already cloned the repository and have the `Dockerfile` in the root directory. For general repository setup (e.g., Claude Code CLI authentication), refer to the sections above.
250282
251283
## Prerequisites
252284
Before building or running the container, ensure the following:
@@ -255,7 +287,7 @@ Before building or running the container, ensure the following:
255287
- **Hardware and Software**:
256288
- OS: macOS (10.15+), Linux (e.g., Ubuntu 20.04+), or Windows (10+ with WSL2 for optimal volume mounting).
257289
- Resources: At least 4GB RAM and 2 CPU cores (Claude requests can be compute-intensive; monitor with `docker stats`).
258-
- Disk: ~500MB for the image, plus space for volumes.
290+
- Disc: ~500MB for the image, plus space for volumes.
259291
- Network: Stable internet for builds (dependency downloads) and runtime (API calls to Anthropic).
260292
- **Optional**:
261293
- Docker Compose: For multi-service or easier configuration management. Install via Docker Desktop or your package manager (e.g., `sudo apt install docker-compose`).
@@ -283,7 +315,7 @@ The `Dockerfile` in the root defines a lightweight Python 3.12-based image with
283315
4. Advanced Build Options:
284316
- No Cache (for fresh builds): `docker build --no-cache -t claude-wrapper:latest .`.
285317
- Platform-Specific (e.g., ARM for Raspberry Pi): `docker build --platform linux/arm64 -t claude-wrapper:arm .`.
286-
- Multi-Stage for Smaller Size: If optimizing, modify the Dockerfile to use multi-stage builds (e.g., separate build and runtime stages).
318+
- Multi-Stage for Smaller Size: If optimising, modify the Dockerfile to use multi-stage builds (e.g., separate build and runtime stages).
287319
288320
If using Docker Compose (see below), build with `docker-compose build`.
289321
@@ -301,6 +333,20 @@ docker run -d -p 8000:8000 \
301333
- `-d`: Detached mode (runs in background).
302334
- `-p 8000:8000`: Maps host port 8000 to the container's 8000 (change left side for host conflicts, e.g., `-p 9000:8000`).
303335
- `-v ~/.claude:/root/.claude`: Mounts your host's authentication directory for persistent subscription tokens (essential for Claude Max access).
336+
- **Working Directory**: By default, Claude Code uses an isolated temp directory inside the container.
337+
338+
### Running with Custom Workspace
339+
To give Claude Code access to a specific directory:
340+
```bash
341+
docker run -d -p 8000:8000 \
342+
-v ~/.claude:/root/.claude \
343+
-v /path/to/your/project:/workspace \
344+
-e CLAUDE_CWD=/workspace \
345+
--name claude-wrapper-container \
346+
claude-wrapper:latest
347+
```
348+
- `-v /path/to/your/project:/workspace`: Mounts your project directory into the container.
349+
- `-e CLAUDE_CWD=/workspace`: Sets Claude's working directory to the mounted workspace.
304350
- `--name claude-wrapper-container`: Names the container for easy management.
305351

306352
### Development Run with Hot Reload
@@ -345,20 +391,21 @@ services:
345391
- Cleanup: `docker system prune` to remove unused images/volumes.
346392

347393
## Custom Configuration Options
348-
Customize the container's behavior through environment variables, volumes, and runtime flags. Most changes don't require rebuilding—just restart the container.
394+
Customise the container's behaviour through environment variables, volumes, and runtime flags. Most changes don't require rebuilding—just restart the container.
349395

350396
### Environment Variables
351-
Env vars override defaults and can be set at runtime with `-e` flags or in `docker-compose.yml` under `environment`. They control auth, server settings, and SDK behavior.
397+
Env vars override defaults and can be set at runtime with `-e` flags or in `docker-compose.yml` under `environment`. They control auth, server settings, and SDK behaviour.
352398

353399
- **Core Server Settings**:
354400
- `PORT=9000`: Changes the internal listening port (default: 8000; update port mapping accordingly).
355401
- `MAX_TIMEOUT=600`: Sets the request timeout in seconds (default: 300; increase for complex Claude queries).
402+
- `CLAUDE_CWD=/path/to/workspace`: Sets Claude Code's working directory (default: isolated temp directory for security).
356403
357404
- **Authentication and Providers**:
358405
- `ANTHROPIC_API_KEY=sk-your-key`: Enables direct API key auth (overrides subscription; generate at console.anthropic.com).
359406
- `CLAUDE_CODE_USE_VERTEX=true`: Switches to Google Vertex AI (requires additional vars like `GOOGLE_APPLICATION_CREDENTIALS=/path/to/creds.json`—mount the file as a volume).
360407
- `CLAUDE_CODE_USE_BEDROCK=true`: Enables AWS Bedrock (set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, etc.).
361-
- `CLAUDE_USE_SUBSCRIPTION=true`: Forces subscription mode (default behavior; set to ensure no API fallback).
408+
- `CLAUDE_USE_SUBSCRIPTION=true`: Forces subscription mode (default behaviour; set to ensure no API fallback).
362409
363410
- **Security and API Protection**:
364411
- `API_KEYS=key1,key2`: Comma-separated list of API keys required for endpoint access (clients must send `Authorization: Bearer <key>`).
@@ -375,7 +422,7 @@ docker run ... -e PORT=9000 -e ANTHROPIC_API_KEY=sk-your-key ...
375422
376423
For persistence across runs, use a `.env` file in the root (e.g., `PORT=8000`) and mount it: `-v $(pwd)/.env:/app/.env`. Load vars in code if required.
377424
378-
### Volumes for Data Persistence and Customization
425+
### Volumes for Data Persistence and Customisation
379426
Volumes mount host directories/files into the container, enabling persistence and config overrides.
380427
381428
- **Authentication Volume (Required for Subscriptions)**: `-v ~/.claude:/root/.claude` – Shares tokens and `settings.json` (edit on host for defaults like `"max_tokens": 8192`; restart container to apply).
@@ -401,7 +448,7 @@ For remote access (e.g., from other machines or production deployment), extend t
401448
### Exposing Locally for Remote Access
402449
- Bind to All Interfaces: Already done with `--host 0.0.0.0`.
403450
- Firewall: Open port 8000 on your host (e.g., `ufw allow 8000` on Ubuntu).
404-
- Tunneling: Use ngrok for temporary exposure: Install ngrok, run `ngrok http 8000`, and use the public URL.
451+
- Tunnelling: Use ngrok for temporary exposure: Install ngrok, run `ngrok http 8000`, and use the public URL.
405452
- Security: Always add `API_KEYS` and use HTTPS (via reverse proxy).
406453
407454
### Deploying to a Remote Server or VPS
@@ -512,7 +559,7 @@ response = client.chat.completions.create(
512559
)
513560

514561
print(response.choices[0].message.content)
515-
# Output: Fast response without tool usage (default behavior)
562+
# Output: Fast response without tool usage (default behaviour)
516563

517564
# Enable tools when you need them (e.g., to read files)
518565
response = client.chat.completions.create(
@@ -600,7 +647,7 @@ curl -X POST http://localhost:8000/v1/chat/completions \
600647
-H "Content-Type: application/json" \
601648
-d '{
602649
"model": "claude-3-5-sonnet-20241022",
603-
"messages": [{"role": "user", "content": "My favorite color is blue."}],
650+
"messages": [{"role": "user", "content": "My favourite color is blue."}],
604651
"session_id": "my-session"
605652
}'
606653

@@ -609,7 +656,7 @@ curl -X POST http://localhost:8000/v1/chat/completions \
609656
-H "Content-Type: application/json" \
610657
-d '{
611658
"model": "claude-3-5-sonnet-20241022",
612-
"messages": [{"role": "user", "content": "What's my favorite color?"}],
659+
"messages": [{"role": "user", "content": "What's my favourite color?"}],
613660
"session_id": "my-session"
614661
}'
615662
```
@@ -748,9 +795,9 @@ All tests should show:
748795
- **Real cost tracking** (e.g., $0.001-0.005 per test call)
749796
- **Accurate token counts** from SDK metadata
750797
751-
## License
798+
## Licence
752799
753-
MIT License
800+
MIT Licence
754801
755802
## Contributing
756803

claude_cli.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import asyncio
22
import json
33
import os
4+
import tempfile
5+
import atexit
6+
import shutil
47
from typing import AsyncGenerator, Dict, Any, Optional, List
58
from pathlib import Path
69
import logging
@@ -13,7 +16,27 @@
1316
class ClaudeCodeCLI:
1417
def __init__(self, timeout: int = 600000, cwd: Optional[str] = None):
1518
self.timeout = timeout / 1000 # Convert ms to seconds
16-
self.cwd = Path(cwd) if cwd else Path.cwd()
19+
self.temp_dir = None
20+
21+
# If cwd is provided (from CLAUDE_CWD env var), use it
22+
# Otherwise create an isolated temp directory
23+
if cwd:
24+
self.cwd = Path(cwd)
25+
# Check if the directory exists
26+
if not self.cwd.exists():
27+
logger.error(f"ERROR: Specified working directory does not exist: {self.cwd}")
28+
logger.error(f"Please create the directory first or unset CLAUDE_CWD to use a temporary directory")
29+
raise ValueError(f"Working directory does not exist: {self.cwd}")
30+
else:
31+
logger.info(f"Using CLAUDE_CWD: {self.cwd}")
32+
else:
33+
# Create isolated temp directory (cross-platform)
34+
self.temp_dir = tempfile.mkdtemp(prefix="claude_code_workspace_")
35+
self.cwd = Path(self.temp_dir)
36+
logger.info(f"Using temporary isolated workspace: {self.cwd}")
37+
38+
# Register cleanup function to remove temp dir on exit
39+
atexit.register(self._cleanup_temp_dir)
1740

1841
# Import auth manager
1942
from auth import auth_manager, validate_claude_code_auth
@@ -233,4 +256,13 @@ def extract_metadata(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
233256
"model": message.get("model")
234257
})
235258

236-
return metadata
259+
return metadata
260+
261+
def _cleanup_temp_dir(self):
262+
"""Clean up temporary directory on exit."""
263+
if self.temp_dir and os.path.exists(self.temp_dir):
264+
try:
265+
shutil.rmtree(self.temp_dir)
266+
logger.info(f"Cleaned up temporary workspace: {self.temp_dir}")
267+
except Exception as e:
268+
logger.warning(f"Failed to clean up temp directory {self.temp_dir}: {e}")

docker-compose.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,11 @@ services:
66
- "8000:8000"
77
volumes:
88
- ~/.claude:/root/.claude
9+
# Optional: Mount a specific workspace directory
10+
# Uncomment and modify the line below to use a custom workspace
11+
# - ./workspace:/workspace
912
environment:
1013
- PORT=8000
14+
# Optional: Set Claude's working directory (defaults to isolated temp dir)
15+
# Uncomment and modify the line below to set a custom working directory
16+
# - CLAUDE_CWD=/workspace

test_docker_workspace.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
# Test script to verify working directory configuration in Docker
3+
4+
echo "Testing Docker workspace configuration..."
5+
echo "========================================="
6+
7+
# Test 1: Default (temp directory)
8+
echo -e "\n1. Testing default configuration (isolated temp dir):"
9+
docker run --rm \
10+
-v ~/.claude:/root/.claude \
11+
claude-wrapper:test \
12+
poetry run python -c "from claude_cli import ClaudeCodeCLI; cli = ClaudeCodeCLI(); print(f'Working directory: {cli.cwd}'); print(f'Is temp dir: {cli.temp_dir is not None}')"
13+
14+
# Test 2: With CLAUDE_CWD environment variable
15+
echo -e "\n2. Testing with CLAUDE_CWD environment variable:"
16+
docker run --rm \
17+
-v ~/.claude:/root/.claude \
18+
-e CLAUDE_CWD=/app \
19+
claude-wrapper:test \
20+
poetry run python -c "import os; from claude_cli import ClaudeCodeCLI; cli = ClaudeCodeCLI(cwd=os.getenv('CLAUDE_CWD')); print(f'Working directory: {cli.cwd}'); print(f'Is temp dir: {cli.temp_dir is not None}')"
21+
22+
# Test 3: With mounted workspace
23+
echo -e "\n3. Testing with mounted workspace:"
24+
mkdir -p /tmp/test_workspace
25+
docker run --rm \
26+
-v ~/.claude:/root/.claude \
27+
-v /tmp/test_workspace:/workspace \
28+
-e CLAUDE_CWD=/workspace \
29+
claude-wrapper:test \
30+
poetry run python -c "import os; from claude_cli import ClaudeCodeCLI; cli = ClaudeCodeCLI(cwd=os.getenv('CLAUDE_CWD')); print(f'Working directory: {cli.cwd}'); print(f'Directory exists: {os.path.exists(cli.cwd)}')"
31+
32+
echo -e "\n========================================="
33+
echo "Docker workspace tests complete!"

0 commit comments

Comments
 (0)