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
# 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
# Optional: Twilio credentials for SMS notifications

5
.gitignore vendored
View File

@ -26,5 +26,10 @@ data/
*.tmp
*.temp
~*
# Claude authentication files (copied during build)
.claude.json
.claude/
# Environment file with credentials
.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
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
# Switch to non-root 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
RUN bash -c 'source /app/.env && \
if [ -n "$TWILIO_ACCOUNT_SID" ] && [ -n "$TWILIO_AUTH_TOKEN" ]; then \
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\"}}"; \
else \
echo "No Twilio credentials found, skipping MCP configuration"; \
@ -60,7 +73,6 @@ WORKDIR /workspace
# Environment variables will be passed from host
ENV NODE_ENV=production
ENV HOME=/home/claude-user
# Start both MCP server and Claude Code
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
- 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
1. **Clone the repository:**
@ -157,6 +166,23 @@ Each project gets:
- `.claude/CLAUDE.md` - Instructions for Claude behavior
- `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
- Docker installed and running

View File

@ -1,171 +1,64 @@
# Claude Docker Project Scratchpad
## 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 ✅
**Phase 1 - Complete MVP:**
- GitHub repository created: https://github.com/VishalJ99/claude-docker
## ✅ COMPLETED PHASES
### Phase 1 - MVP (Complete)
- Docker setup with Claude Code + Twilio MCP integration
- Wrapper script (`claude-docker.sh`) for easy invocation
- Auto .claude directory setup with MCP configuration
- Installation script for zshrc alias
- SMS notifications via Twilio MCP server
- Wrapper script for easy invocation
- Auto .claude directory setup
- Full autonomous permissions with --dangerously-skip-permissions
- 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 - Security & Persistence Enhancements:**
### Phase 2 - Authentication Persistence (Complete)
- **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
**Problem:** Need to re-login to Claude Code every time container starts
## 🎯 CURRENT FOCUS
**Phase 3 - Smart SMS Notifications:**
**Research Findings:**
- Claude Code stores auth tokens in `~/.claude/.credentials.json`
- 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
### Next Task: Prompt Engineering for SMS Notifications
**Goal:** Configure Claude to automatically send completion SMS to `$TWILIO_TO_NUMBER`
**Implementation Completed:**
1. **Created persistent directory structure:**
- Host: `~/.claude-docker/claude-home`
- Container: `/home/claude-user/.claude`
- Mounted with read/write permissions
**Implementation Plan:**
1. Update CLAUDE.md template with SMS notification instructions
2. Add completion detection logic
3. Integrate with existing Twilio MCP server
4. Test notification flow
2. **Updated Docker setup:**
- Created non-root user `claude-user` for better security
- Set proper ownership and permissions
- Added volume mount for Claude home directory
## 📚 KEY INSIGHTS FROM AUTHENTICATION JOURNEY
3. **Enhanced startup script:**
- Checks for existing `.credentials.json` on startup
- Notifies user if auth exists or login needed
- Credentials persist across container restarts
### Critical Discovery: ~/.claude.json + MCP Scope
- Claude Code requires BOTH `~/.claude.json` (user profile) AND `~/.claude/.credentials.json` (tokens)
- MCP servers default to "local" scope (project-specific) - need "user" scope for persistence
- 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
**Implementation based on devcontainer's init-firewall.sh:**
**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
## 🔮 FUTURE ENHANCEMENTS
- Network security with firewall (iptables + ipset)
- Shell history persistence between sessions
- Git configuration persistence
- Custom shell aliases and environment
## Direction & Vision
**Security-First Autonomous Environment:**
- Maintain full Claude autonomy within projects
- Add network security layer to prevent unauthorized access
- Enhance user experience with persistent shell history
- Keep container lightweight and fast
- Ensure easy setup and maintenance
## 📋 DECISIONS LOG
- MCP integration using user scope for persistence
- Authentication files baked into Docker image at build time
- Single container approach (no Docker Compose)
- Simplified rebuild logic (only when image missing)
- SMS via `@yiyang.1i/sms-mcp-server` with Auth Token
## Decisions Log
- Using MCP (Model Context Protocol) for Twilio integration instead of direct API
- Single container approach (no Docker Compose needed)
- API keys via .env file
- Context persistence via scratchpad.md files
- Simplified settings.json to only include MCP config (no redundant allowedTools)
- **NEW:** Adding firewall for network security
- **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
## 🔗 QUICK REFERENCES
- **Install:** `./scripts/install.sh`
- **Usage:** `claude-docker` (from any project directory)
- **Config:** `~/.claude-docker/.env`
- **Force rebuild:** `docker rmi claude-docker:latest`
- **SMS command:** `twilio__send_text`
- **Repository:** https://github.com/VishalJ99/claude-docker

View File

@ -38,28 +38,24 @@ fi
NEED_REBUILD=false
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
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
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"
# Clean up copied auth files
rm -f "$PROJECT_ROOT/.claude.json"
rm -rf "$PROJECT_ROOT/.claude"
fi
# 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();