Run escalation system as a remote Docker service — Hooks communicate via HTTP instead of local binary.
cd claude-escalate/docker-compose
docker-compose up -dAccess dashboard: http://localhost:8077
Edit ~/.claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{
"type": "command",
"command": "/Users/slawomirskowron/.claude/hooks/escalation-service-hook.sh",
"timeout": 3
}
]
}
}# Check service health
curl http://localhost:9000/health
# Get current status
curl http://localhost:9000/api/status
# Test escalate
curl -X POST http://localhost:9000/api/escalate \
-H "Content-Type: application/json" \
-d '{"target":"opus"}'Claude Code (Local)
↓
Hooks (escalation-service-hook.sh)
↓ HTTP/JSON
Docker Service (Port 9000)
├─ Escalation Manager (In-container)
├─ Stats Tracking (In-container)
└─ Dashboard (Port 8077)
↓
Persistent Volume (/data/escalation)
✅ Decoupled: Escalation system runs independently
✅ Portable: Same container across machines
✅ Observable: Centralized logs and metrics
✅ Scalable: Can run multiple services, load-balance requests
✅ Easy Updates: Update container without touching local hooks
Health check response:
{"status":"healthy","timestamp":"2026-04-25T10:00:00Z"}Current escalation state:
{
"currentModel": "sonnet",
"effort": "medium",
"sessionActive": true,
"sessionAge": 120,
"lastEscalation": "2026-04-25T10:00:00Z"
}Request:
{"target":"opus"}Response:
{"success":true,"model":"opus","timestamp":"2026-04-25T10:00:00Z"}Request:
{"reason":"success_signal"}Response:
{"success":true,"model":"sonnet","timestamp":"2026-04-25T10:00:00Z","cascaded":true}Request:
{"level":"high"}Response:
{"success":true,"level":"high","model":"opus"}Detailed metrics:
{
"totalEscalations": 5,
"totalDeescalations": 3,
"currentModel": "sonnet",
"sessions": [
{
"id": "session-1",
"startTime": "2026-04-25T09:00:00Z",
"initialModel": "haiku",
"finalModel": "sonnet",
"tokensCost": 250,
"saved": 150
}
]
}Environment variables in docker-compose.yml:
environment:
- PORT=9000 # Service API port
- DASHBOARD_PORT=8077 # Web dashboard port
- DATA_DIR=/data/escalation # Persistent data directory
- SERVICE_TIMEOUT=5 # Request timeout (seconds)
- LOG_LEVEL=info # Log verbosityIf the Docker service is unavailable, hooks automatically fall back to the local binary:
# This hook checks health first
call_service "/api/escalate" ...
# If service down, uses fallback
$FALLBACK_BINARY escalate --to opusThis ensures hooks never fail completely — they degrade gracefully.
docker-compose logs -f escalation-servicedocker-compose restart escalation-servicedocker-compose pull
docker-compose up -ddocker volume ls
docker volume inspect claude-escalate_escalation-data
docker run -v claude-escalate_escalation-data:/data alpine ls -la /data/escalationDashboard shows real-time metrics:
- Current model and cost
- Active sessions
- Total escalations/de-escalations
- Token cost analysis
- Session history
Access: http://localhost:8077
To use a local binary while developing:
# Stop Docker service
docker-compose down
# Run local binary directly
~/.local/bin/escalation-manager dashboard 8077
# Hooks automatically use local binary (no service to connect to)docker-compose logs escalation-service
docker ps | grep escalationcurl -v http://localhost:9000/health
# Should see 200 response with JSON# Test manually
curl -X POST http://localhost:9000/api/escalate \
-H "Content-Type: application/json" \
-d '{"target":"opus"}'
# Should see success response# Verify volume mounted correctly
docker inspect claude-escalation-service | grep -A 5 Mounts- Start service:
docker-compose up -d - Verify:
curl http://localhost:9000/health - Update hooks: Add to settings.json
- Test: Use
/escalatecommands in Claude Code - Monitor: Watch dashboard at http://localhost:8077