Implement zero-friction authentication persistence with MCP user scope

Major breakthrough solving the authentication chicken-and-egg problem:

Key Changes:
- Copy ~/.claude.json and ~/.claude/ during Docker build for baked-in auth
- Add -s user flag to claude mcp add-json for persistent MCP servers
- Simplify rebuild logic to prevent unnecessary rebuilds
- Update documentation with rebuild instructions

Technical Details:
- Authentication files placed before USER switch in Dockerfile
- MCP configuration now persists across all sessions
- Rebuild only occurs when image doesn't exist
- Clean separation of build vs runtime concerns

Result: Users authenticate once on host, then zero login prompts forever.
SMS notifications ready immediately on container start.
This commit is contained in:
Vishal Jain 2025-06-17 22:27:11 +01:00
parent d9bf0f4b53
commit 7cd765b756
7 changed files with 106 additions and 233 deletions

View File

@ -1,7 +1,7 @@
# Copy this file to .env and fill in your credentials # Copy this file to .env and fill in your credentials
# The .env file will be baked into the Docker image during build # The .env file will be baked into the Docker image during build
# Required for Claude Code # Required for Claude Code if not using via subscription.
ANTHROPIC_API_KEY=your_anthropic_api_key_here ANTHROPIC_API_KEY=your_anthropic_api_key_here
# Optional: Twilio credentials for SMS notifications # Optional: Twilio credentials for SMS notifications

5
.gitignore vendored
View File

@ -26,5 +26,10 @@ data/
*.tmp *.tmp
*.temp *.temp
~* ~*
# Claude authentication files (copied during build)
.claude.json
.claude/
# Environment file with credentials # Environment file with credentials
.env .env

View File

@ -39,17 +39,30 @@ RUN chmod +x /app/startup.sh
# This enables one-time setup - no need for .env in project directories # This enables one-time setup - no need for .env in project directories
COPY .env /app/.env COPY .env /app/.env
# Set proper ownership # Copy Claude authentication files from host
# Note: These must exist - host must have authenticated Claude Code first
COPY .claude.json /tmp/.claude.json
COPY .claude /tmp/.claude
# Move auth files to proper location before switching user
RUN cp /tmp/.claude.json /home/claude-user/.claude.json && \
cp -r /tmp/.claude/* /home/claude-user/.claude/ && \
rm -rf /tmp/.claude*
# Set proper ownership for everything
RUN chown -R claude-user:claude-user /app /home/claude-user RUN chown -R claude-user:claude-user /app /home/claude-user
# Switch to non-root user # Switch to non-root user
USER claude-user USER claude-user
# Set HOME immediately after switching user
ENV HOME=/home/claude-user
# Configure MCP server during build if Twilio credentials are provided # Configure MCP server during build if Twilio credentials are provided
RUN bash -c 'source /app/.env && \ RUN bash -c 'source /app/.env && \
if [ -n "$TWILIO_ACCOUNT_SID" ] && [ -n "$TWILIO_AUTH_TOKEN" ]; then \ if [ -n "$TWILIO_ACCOUNT_SID" ] && [ -n "$TWILIO_AUTH_TOKEN" ]; then \
echo "Configuring Twilio MCP server..." && \ echo "Configuring Twilio MCP server..." && \
/usr/local/bin/claude mcp add-json twilio \ /usr/local/bin/claude mcp add-json twilio -s user \
"{\"command\":\"npx\",\"args\":[\"-y\",\"@yiyang.1i/sms-mcp-server\"],\"env\":{\"ACCOUNT_SID\":\"$TWILIO_ACCOUNT_SID\",\"AUTH_TOKEN\":\"$TWILIO_AUTH_TOKEN\",\"FROM_NUMBER\":\"$TWILIO_FROM_NUMBER\"}}"; \ "{\"command\":\"npx\",\"args\":[\"-y\",\"@yiyang.1i/sms-mcp-server\"],\"env\":{\"ACCOUNT_SID\":\"$TWILIO_ACCOUNT_SID\",\"AUTH_TOKEN\":\"$TWILIO_AUTH_TOKEN\",\"FROM_NUMBER\":\"$TWILIO_FROM_NUMBER\"}}"; \
else \ else \
echo "No Twilio credentials found, skipping MCP configuration"; \ echo "No Twilio credentials found, skipping MCP configuration"; \
@ -60,7 +73,6 @@ WORKDIR /workspace
# Environment variables will be passed from host # Environment variables will be passed from host
ENV NODE_ENV=production ENV NODE_ENV=production
ENV HOME=/home/claude-user
# Start both MCP server and Claude Code # Start both MCP server and Claude Code
ENTRYPOINT ["/app/startup.sh"] ENTRYPOINT ["/app/startup.sh"]

View File

@ -10,6 +10,15 @@ A Docker container setup for running Claude Code with full autonomous permission
- Auto-configures Claude settings for seamless operation - Auto-configures Claude settings for seamless operation
- Simple one-command setup and usage - Simple one-command setup and usage
## Prerequisites
**Important**: Before building the Docker image, you must authenticate Claude Code on your host system:
1. Install Claude Code: `npm install -g @anthropic-ai/claude-code`
2. Run `claude` and complete the authentication flow
3. Verify authentication files exist: `ls ~/.claude.json ~/.claude/`
The Docker build will copy your authentication from these locations.
## Quick Start ## Quick Start
1. **Clone the repository:** 1. **Clone the repository:**
@ -157,6 +166,23 @@ Each project gets:
- `.claude/CLAUDE.md` - Instructions for Claude behavior - `.claude/CLAUDE.md` - Instructions for Claude behavior
- `scratchpad.md` - Project context file - `scratchpad.md` - Project context file
### Rebuilding the Image
The Docker image is built only once when you first run `claude-docker`. To force a rebuild:
```bash
# Remove the existing image
docker rmi claude-docker:latest
# Next run of claude-docker will rebuild
claude-docker
```
Rebuild when you:
- Update your .env file with new credentials
- Update the Claude Docker repository
- Want to refresh the authentication files
## Requirements ## Requirements
- Docker installed and running - Docker installed and running

View File

@ -1,171 +1,64 @@
# Claude Docker Project Scratchpad # Claude Docker Project Scratchpad
## Project Overview ## Project Overview
Building a Docker container that runs Claude Code with full autonomous permissions and Twilio SMS notifications upon task completion. Docker container for Claude Code with full autonomous permissions, authentication persistence, and Twilio SMS notifications.
## What Was Done ✅ ## ✅ COMPLETED PHASES
**Phase 1 - Complete MVP:**
- GitHub repository created: https://github.com/VishalJ99/claude-docker ### Phase 1 - MVP (Complete)
- Docker setup with Claude Code + Twilio MCP integration - Docker setup with Claude Code + Twilio MCP integration
- Wrapper script (`claude-docker.sh`) for easy invocation - Wrapper script for easy invocation
- Auto .claude directory setup with MCP configuration - Auto .claude directory setup
- Installation script for zshrc alias
- SMS notifications via Twilio MCP server
- Full autonomous permissions with --dangerously-skip-permissions - Full autonomous permissions with --dangerously-skip-permissions
- Context persistence via scratchpad.md files - Context persistence via scratchpad.md files
- Complete documentation and examples
- **✅ WORKING** - All startup issues resolved, Docker container launches Claude Code successfully
## Next Steps 🎯 ### Phase 2 - Authentication Persistence (Complete)
**Phase 2 - Security & Persistence Enhancements:** - **SOLVED**: Authentication files copied during Docker build
- **SOLVED**: MCP servers persist with user scope
- **SOLVED**: No unnecessary rebuilds
- **RESULT**: Zero-friction experience - no login prompts, SMS ready instantly
### 1. Authentication Persistence (HIGH Priority) - ✅ COMPLETED ## 🎯 CURRENT FOCUS
**Problem:** Need to re-login to Claude Code every time container starts **Phase 3 - Smart SMS Notifications:**
**Research Findings:** ### Next Task: Prompt Engineering for SMS Notifications
- Claude Code stores auth tokens in `~/.claude/.credentials.json` **Goal:** Configure Claude to automatically send completion SMS to `$TWILIO_TO_NUMBER`
- Known issues: #1222 (persistent auth warnings), #1676 (logout after restart)
- The devcontainer mounts `/home/node/.claude` for config persistence
- But auth tokens are NOT persisted properly even in devcontainer
**Implementation Completed:** **Implementation Plan:**
1. **Created persistent directory structure:** 1. Update CLAUDE.md template with SMS notification instructions
- Host: `~/.claude-docker/claude-home` 2. Add completion detection logic
- Container: `/home/claude-user/.claude` 3. Integrate with existing Twilio MCP server
- Mounted with read/write permissions 4. Test notification flow
2. **Updated Docker setup:** ## 📚 KEY INSIGHTS FROM AUTHENTICATION JOURNEY
- Created non-root user `claude-user` for better security
- Set proper ownership and permissions
- Added volume mount for Claude home directory
3. **Enhanced startup script:** ### Critical Discovery: ~/.claude.json + MCP Scope
- Checks for existing `.credentials.json` on startup - Claude Code requires BOTH `~/.claude.json` (user profile) AND `~/.claude/.credentials.json` (tokens)
- Notifies user if auth exists or login needed - MCP servers default to "local" scope (project-specific) - need "user" scope for persistence
- Credentials persist across container restarts - Authentication can be baked into Docker image during build
- Simple rebuild logic (only if image missing) prevents unnecessary rebuilds
**Result:** Users now login once and authentication persists forever! ### Technical Implementation
- Copy auth files during Docker build, not runtime mounting
- Use `-s user` flag for MCP persistence across sessions
- Files placed at correct locations before user switch in Dockerfile
### 2. Network Security (High Priority) - PLANNED ## 🔮 FUTURE ENHANCEMENTS
**Implementation based on devcontainer's init-firewall.sh:** - Network security with firewall (iptables + ipset)
- Shell history persistence between sessions
**Key Components:**
1. **Firewall Script Features:**
- Uses iptables with default DROP policy
- ipset for managing allowed IP ranges
- Dynamic IP resolution for allowed domains
- Verification of connectivity post-setup
2. **Allowed Domains Configuration:**
```yaml
allowed_domains:
- api.anthropic.com # Claude API
- api.twilio.com # SMS notifications
- github.com # Git operations
- raw.githubusercontent.com
- registry.npmjs.org # Package management
- pypi.org # Python packages
blocked_paths: # File system restrictions
- /etc
- /root
- ~/.ssh
```
3. **User-Friendly Setup:**
- Simple YAML config file for rules
- Easy enable/disable of firewall
- Logging of blocked attempts
- Graceful degradation if firewall fails
### 3. Shell History Persistence (Medium Priority)
- Add persistent bash/zsh history between container sessions
- Mount history file to host directory
- Implement history management similar to Claude dev container
- Ensure commands persist across sessions
### 4. Additional Persistence Features (Medium Priority)
- Persistent npm cache for faster startups
- Git configuration persistence - Git configuration persistence
- Custom shell aliases and environment
## Direction & Vision ## 📋 DECISIONS LOG
**Security-First Autonomous Environment:** - MCP integration using user scope for persistence
- Maintain full Claude autonomy within projects - Authentication files baked into Docker image at build time
- Add network security layer to prevent unauthorized access - Single container approach (no Docker Compose)
- Enhance user experience with persistent shell history - Simplified rebuild logic (only when image missing)
- Keep container lightweight and fast - SMS via `@yiyang.1i/sms-mcp-server` with Auth Token
- Ensure easy setup and maintenance
## Decisions Log ## 🔗 QUICK REFERENCES
- Using MCP (Model Context Protocol) for Twilio integration instead of direct API - **Install:** `./scripts/install.sh`
- Single container approach (no Docker Compose needed) - **Usage:** `claude-docker` (from any project directory)
- API keys via .env file - **Config:** `~/.claude-docker/.env`
- Context persistence via scratchpad.md files - **Force rebuild:** `docker rmi claude-docker:latest`
- Simplified settings.json to only include MCP config (no redundant allowedTools) - **SMS command:** `twilio__send_text`
- **NEW:** Adding firewall for network security - **Repository:** https://github.com/VishalJ99/claude-docker
- **NEW:** Adding shell history persistence like Claude dev container
- **NEW (2024-12-06):** Focus on auth persistence first before firewall implementation
- **COMPLETED (2024-12-06):** Auth persistence via mounted ~/.claude directory
## Notes & Context
- Repository: https://github.com/VishalJ99/claude-docker
- Using --dangerously-skip-permissions flag for full autonomy
- Twilio MCP server runs via Claude's MCP config (not as separate process)
- Uses @twilio-alpha/mcp package with API Key/Secret authentication
- Container auto-removes on exit for clean state
- Project directory mounted at /workspace
- Need to research Claude dev container's init-firewall.sh implementation
- Need to research their history persistence mechanism
- **Fixed startup issues:**
- Changed executable from `claude-code` to `claude` in startup.sh
- Fixed .env parsing to handle comments properly using `set -a`/`source`
- Added explicit PATH for npm global binaries
- Maintained separation: `claude-docker` (host) vs `claude` (container)
- **Current working state:** Container launches successfully, authentication required each session
- **Auth Persistence Research (2024-12-06):**
- Claude Code has known issues with auth persistence
- Tokens stored in temp locations that get cleared
- Need to find exact token storage location and persist it
## MCP Integration Update (2024-12-17)
### ✅ COMPLETED: Simplified Twilio MCP Integration
**What Changed:**
1. **Switched MCP Server:** From `@twilio-alpha/mcp` (API Key/Secret) to `@yiyang.1i/sms-mcp-server` (Auth Token)
2. **Simplified Configuration:** MCP setup now happens during Docker build instead of runtime
3. **Removed Complexity:** No more mcp-config.json or environment variable substitution
**Implementation Details:**
1. **Updated .env.example:**
- Removed: `TWILIO_API_KEY` and `TWILIO_API_SECRET`
- Added: `TWILIO_AUTH_TOKEN`
- Kept: `TWILIO_ACCOUNT_SID`, `TWILIO_FROM_NUMBER`, `TWILIO_TO_NUMBER`
2. **Updated Dockerfile:**
- Removed global installation of `@twilio-alpha/mcp`
- Added MCP configuration during build using `claude mcp add-json` command
- MCP server is configured if Twilio credentials are present in .env
3. **Simplified startup.sh:**
- Removed all MCP configuration logic
- Just loads environment variables and starts Claude
- Shows Twilio status on startup
4. **Removed Files:**
- `config/mcp-config.json` (no longer needed)
- `config/` directory (now empty)
**Result:**
- MCP configuration is baked into the Docker image at build time
- No runtime configuration needed
- Simpler, more reliable setup
- SMS capability available via `twilio__send_text` command
## Quick References
- Install: `./scripts/install.sh`
- Usage: `claude-docker` (from any project directory)
- Config: `~/.claude-docker/.env`
- Repo: https://github.com/VishalJ99/claude-docker
- Claude dev container: https://github.com/anthropics/claude-code/tree/main/.devcontainer

View File

@ -38,28 +38,24 @@ fi
NEED_REBUILD=false NEED_REBUILD=false
if ! docker images | grep -q "claude-docker"; then if ! docker images | grep -q "claude-docker"; then
echo "Building Claude Docker image with your user permissions..." echo "Building Claude Docker image for first time..."
NEED_REBUILD=true NEED_REBUILD=true
elif ! docker image inspect claude-docker:latest | grep -q "USER_UID.*$(id -u)" 2>/dev/null; then
echo "Rebuilding Claude Docker image to match your user permissions..."
NEED_REBUILD=true
elif [ -f "$ENV_FILE" ]; then
# Check if .env is newer than the Docker image
IMAGE_CREATED=$(docker inspect -f '{{.Created}}' claude-docker:latest 2>/dev/null)
if [ -n "$IMAGE_CREATED" ]; then
IMAGE_TIMESTAMP=$(date -d "$IMAGE_CREATED" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${IMAGE_CREATED%%.*}" +%s 2>/dev/null)
ENV_TIMESTAMP=$(stat -c %Y "$ENV_FILE" 2>/dev/null || stat -f %m "$ENV_FILE" 2>/dev/null)
if [ -n "$IMAGE_TIMESTAMP" ] && [ -n "$ENV_TIMESTAMP" ] && [ "$ENV_TIMESTAMP" -gt "$IMAGE_TIMESTAMP" ]; then
echo "⚠️ .env file has been updated since last build"
echo " Rebuilding to include new credentials..."
NEED_REBUILD=true
fi
fi
fi fi
if [ "$NEED_REBUILD" = true ]; then if [ "$NEED_REBUILD" = true ]; then
# Copy authentication files to build context
if [ -f "$HOME/.claude.json" ]; then
cp "$HOME/.claude.json" "$PROJECT_ROOT/.claude.json"
fi
if [ -d "$HOME/.claude" ]; then
cp -r "$HOME/.claude" "$PROJECT_ROOT/.claude"
fi
docker build --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) -t claude-docker:latest "$PROJECT_ROOT" docker build --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) -t claude-docker:latest "$PROJECT_ROOT"
# Clean up copied auth files
rm -f "$PROJECT_ROOT/.claude.json"
rm -rf "$PROJECT_ROOT/.claude"
fi fi
# Ensure the claude-home directory exists # Ensure the claude-home directory exists

View File

@ -1,59 +0,0 @@
#!/usr/bin/env node
// Test script to verify Twilio SMS functionality
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const fromNumber = process.env.TWILIO_FROM_NUMBER;
const toNumber = process.env.TWILIO_TO_NUMBER;
console.log('Twilio Test Configuration:');
console.log(`Account SID: ${accountSid?.substring(0, 10)}...`);
console.log(`Auth Token: ${authToken ? '***' + authToken.substring(authToken.length - 4) : 'Not set'}`);
console.log(`From: ${fromNumber}`);
console.log(`To: ${toNumber}`);
// Using Twilio REST API directly
const https = require('https');
const auth = Buffer.from(`${accountSid}:${authToken}`).toString('base64');
const data = new URLSearchParams({
To: toNumber,
From: fromNumber,
Body: 'MCP is working! This is a test message from Claude Docker.'
});
const options = {
hostname: 'api.twilio.com',
port: 443,
path: `/2010-04-01/Accounts/${accountSid}/Messages.json`,
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': data.toString().length
}
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
if (res.statusCode === 201) {
console.log('\n✅ SMS sent successfully!');
const response = JSON.parse(body);
console.log(`Message SID: ${response.sid}`);
console.log(`Status: ${response.status}`);
} else {
console.error('\n❌ Failed to send SMS');
console.error(`Status: ${res.statusCode}`);
console.error(`Response: ${body}`);
}
});
});
req.on('error', (e) => {
console.error(`Problem with request: ${e.message}`);
});
req.write(data.toString());
req.end();