add claude [ci skip]
This commit is contained in:
parent
9ef4d38d51
commit
ffa80f0df6
18 changed files with 3026 additions and 95 deletions
175
.claude/skills/bluestacks-burp-interception/SKILL.md
Normal file
175
.claude/skills/bluestacks-burp-interception/SKILL.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
---
|
||||
name: bluestacks-burp-interception
|
||||
description: |
|
||||
Intercept Android app HTTPS traffic using BlueStacks and Burp Suite on macOS.
|
||||
Use when: (1) Need to analyze Android app API calls, (2) App ignores HTTP proxy,
|
||||
(3) App uses SSL pinning that blocks interception, (4) Need to install Burp CA
|
||||
as system certificate. Covers ADB setup, proxy configuration, Zygisk SSL unpinning,
|
||||
and Magisk trustusercerts module for system CA installation.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2026-01-24
|
||||
---
|
||||
|
||||
# BlueStacks + Burp Suite HTTPS Traffic Interception
|
||||
|
||||
## Problem
|
||||
You want to intercept HTTPS traffic from an Android app running in BlueStacks to analyze
|
||||
API calls, but the app either ignores the proxy or uses SSL certificate pinning.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
- Running BlueStacks on macOS with Burp Suite
|
||||
- App traffic not appearing in Burp Suite
|
||||
- App crashes or refuses to connect when proxy is set
|
||||
- Need to bypass SSL pinning for security testing/research
|
||||
|
||||
## Prerequisites
|
||||
- BlueStacks with Magisk (kitsune variant) and root enabled
|
||||
- Zygisk-SSL-Unpinning module installed
|
||||
- trustusercerts Magisk module installed
|
||||
- Android SDK installed (for ADB)
|
||||
- Burp Suite running on port 8080
|
||||
|
||||
## Solution
|
||||
|
||||
### Step 1: Connect ADB to BlueStacks
|
||||
|
||||
```bash
|
||||
# ADB location on macOS (Android SDK)
|
||||
ADB=~/Library/Android/sdk/platform-tools/adb
|
||||
|
||||
# Connect to BlueStacks
|
||||
$ADB connect localhost:5555
|
||||
|
||||
# Verify connection
|
||||
$ADB devices
|
||||
# Should show: emulator-5554 or localhost:5555
|
||||
```
|
||||
|
||||
Note: BlueStacks runs **arm64-v8a** (not x86 as you might expect).
|
||||
|
||||
### Step 2: Set HTTP Proxy
|
||||
|
||||
Use your Mac's WiFi IP address (not 10.0.2.2 or localhost):
|
||||
|
||||
```bash
|
||||
# Get Mac WiFi IP
|
||||
IP=$(ipconfig getifaddr en0)
|
||||
|
||||
# Set proxy (Burp default port 8080)
|
||||
$ADB shell settings put global http_proxy ${IP}:8080
|
||||
|
||||
# Verify
|
||||
$ADB shell settings get global http_proxy
|
||||
|
||||
# Disable proxy when done
|
||||
$ADB shell settings put global http_proxy :0
|
||||
```
|
||||
|
||||
### Step 3: Configure SSL Unpinning for Target App
|
||||
|
||||
```bash
|
||||
# Find app package name
|
||||
$ADB shell pm list packages | grep <keyword>
|
||||
|
||||
# Edit config
|
||||
$ADB shell "su -c 'cat > /data/local/tmp/zyg.ssl/config.json << EOF
|
||||
{
|
||||
\"targets\": [
|
||||
{
|
||||
\"pkg_name\" : \"com.example.app\",
|
||||
\"enable\": true,
|
||||
\"start_safe\": true,
|
||||
\"start_delay\": 1000
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF'"
|
||||
|
||||
# Restart the app
|
||||
$ADB shell am force-stop com.example.app
|
||||
$ADB shell monkey -p com.example.app -c android.intent.category.LAUNCHER 1
|
||||
|
||||
# Verify SSL unpinning is active
|
||||
$ADB shell "logcat -d | grep -i ZygiskSSL | tail -10"
|
||||
# Should show: "App detected: com.example.app" and "[*] SSL UNPINNING [#]"
|
||||
```
|
||||
|
||||
### Step 4: Install Burp CA as System Certificate
|
||||
|
||||
```bash
|
||||
# Download Burp CA cert
|
||||
curl -x http://127.0.0.1:8080 http://burp/cert -o /tmp/burp-cert.der
|
||||
|
||||
# Convert to PEM
|
||||
openssl x509 -inform DER -in /tmp/burp-cert.der -out /tmp/burp-cert.pem
|
||||
|
||||
# Get hash for Android cert store naming
|
||||
HASH=$(openssl x509 -inform PEM -subject_hash_old -in /tmp/burp-cert.pem | head -1)
|
||||
cp /tmp/burp-cert.pem /tmp/${HASH}.0
|
||||
|
||||
# Push to device
|
||||
$ADB push /tmp/${HASH}.0 /sdcard/
|
||||
|
||||
# Install via trustusercerts Magisk module
|
||||
$ADB shell "su -c 'cp /sdcard/${HASH}.0 /data/adb/modules/trustusercerts/system/etc/security/cacerts/'"
|
||||
$ADB shell "su -c 'chmod 644 /data/adb/modules/trustusercerts/system/etc/security/cacerts/${HASH}.0'"
|
||||
|
||||
# Reboot required for Magisk overlay
|
||||
$ADB shell "su -c 'reboot'"
|
||||
|
||||
# After reboot, verify cert is in system store
|
||||
$ADB shell "su -c 'ls /system/etc/security/cacerts/${HASH}.0'"
|
||||
```
|
||||
|
||||
### Step 5: Test Interception
|
||||
|
||||
1. Re-enable proxy after reboot: `$ADB shell settings put global http_proxy ${IP}:8080`
|
||||
2. Launch target app
|
||||
3. Check Burp Suite → Proxy → HTTP history for requests
|
||||
|
||||
## Verification
|
||||
|
||||
- Proxy set: `adb shell settings get global http_proxy` returns `<ip>:8080`
|
||||
- SSL unpinning active: `logcat | grep ZygiskSSL` shows "SSL UNPINNING"
|
||||
- Burp CA installed: `ls /system/etc/security/cacerts/<hash>.0` exists
|
||||
- Traffic visible in Burp Suite HTTP history
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| No traffic in Burp | Proxy not set | Check `settings get global http_proxy` |
|
||||
| App shows SSL error | Cert not installed | Verify cert in system store, reboot |
|
||||
| SSL unpinning not working | Config not loaded | Force-stop app, check config.json syntax |
|
||||
| ADB connection refused | BlueStacks ADB disabled | Enable in BlueStacks Settings → Advanced |
|
||||
| Wrong cert hash | Using wrong openssl flag | Use `subject_hash_old` not `subject_hash` |
|
||||
|
||||
## Notes
|
||||
|
||||
- BlueStacks runs arm64-v8a, so Zygisk modules need arm64 support
|
||||
- The trustusercerts module copies certs at boot via Magisk overlay
|
||||
- System partition is read-only; use Magisk modules instead of direct mounting
|
||||
- Burp cert hash is typically `9a5ba575` but verify for your instance
|
||||
- Some apps may use additional protections (root detection, Frida detection)
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Set proxy
|
||||
adb shell settings put global http_proxy <ip>:8080
|
||||
|
||||
# Disable proxy
|
||||
adb shell settings put global http_proxy :0
|
||||
|
||||
# Check SSL unpinning logs
|
||||
adb shell "logcat -d | grep -i ZygiskSSL"
|
||||
|
||||
# Force restart app
|
||||
adb shell am force-stop <package> && adb shell monkey -p <package> -c android.intent.category.LAUNCHER 1
|
||||
```
|
||||
|
||||
## References
|
||||
- [Zygisk-SSL-Unpinning](https://github.com/m0szy/Zygisk-SSL-Unpinning)
|
||||
- [MagiskTrustUserCerts](https://github.com/NVISOsecurity/MagiskTrustUserCerts)
|
||||
- [Burp Suite Documentation](https://portswigger.net/burp/documentation)
|
||||
1
.claude/skills/claudeception
Submodule
1
.claude/skills/claudeception
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 7d7f5915f90db26e1a3fc52db6ac2e68d6d705a2
|
||||
310
.claude/skills/fastapi-svelte-gpu-webui/SKILL.md
Normal file
310
.claude/skills/fastapi-svelte-gpu-webui/SKILL.md
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
---
|
||||
name: fastapi-svelte-gpu-webui
|
||||
description: |
|
||||
Pattern for building web UIs for GPU-based CLI tools. Use when:
|
||||
(1) Wrapping a command-line tool with a web interface, (2) Building job queue
|
||||
systems for long-running GPU tasks, (3) Creating file upload/download workflows,
|
||||
(4) Need real-time progress updates via WebSocket, (5) Deploying to Kubernetes
|
||||
with GPU scheduling. Covers FastAPI backend, Svelte 5 frontend, NFS storage,
|
||||
and Terraform deployment.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2025-01-31
|
||||
---
|
||||
|
||||
# FastAPI + Svelte GPU WebUI Pattern
|
||||
|
||||
## Problem
|
||||
Many powerful tools are command-line only, making them inaccessible to non-technical
|
||||
users. Building a web UI requires handling file uploads, job queuing, progress tracking,
|
||||
and GPU resource scheduling.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
- You have a CLI tool that does heavy processing (ML inference, media conversion, etc.)
|
||||
- Want to add a web interface for easier access
|
||||
- Need to track long-running job progress
|
||||
- Deploying to Kubernetes with GPU nodes
|
||||
- Files need to persist across pod restarts (NFS storage)
|
||||
|
||||
## Solution Overview
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
project-web/
|
||||
├── backend/
|
||||
│ ├── main.py # FastAPI app
|
||||
│ ├── api/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── routes.py # REST endpoints
|
||||
│ ├── services/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── converter.py # CLI wrapper + job manager
|
||||
│ ├── models/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── schemas.py # Pydantic models
|
||||
│ └── requirements.txt
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── App.svelte
|
||||
│ │ ├── lib/
|
||||
│ │ │ ├── FileUpload.svelte
|
||||
│ │ │ ├── JobsList.svelte
|
||||
│ │ │ └── ProgressBar.svelte
|
||||
│ │ └── stores/
|
||||
│ │ └── jobs.js
|
||||
│ ├── package.json
|
||||
│ └── vite.config.js
|
||||
├── Dockerfile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Backend: Job Manager Pattern
|
||||
```python
|
||||
# services/converter.py
|
||||
import asyncio
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable
|
||||
import subprocess
|
||||
|
||||
class Job:
|
||||
id: str
|
||||
filename: str
|
||||
status: str # pending, processing, completed, failed
|
||||
progress: float
|
||||
created_at: datetime
|
||||
output_file: Optional[str]
|
||||
error: Optional[str]
|
||||
|
||||
class JobManager:
|
||||
def __init__(self, storage_path: str = "/mnt"):
|
||||
self.storage_path = Path(storage_path)
|
||||
self.jobs: dict[str, Job] = {}
|
||||
self.progress_callbacks: dict[str, list[Callable]] = {}
|
||||
|
||||
def create_job(self, filename: str, **options) -> Job:
|
||||
job_id = str(uuid.uuid4())
|
||||
job = Job(
|
||||
id=job_id,
|
||||
filename=filename,
|
||||
status="pending",
|
||||
progress=0.0,
|
||||
created_at=datetime.now(),
|
||||
**options
|
||||
)
|
||||
self.jobs[job_id] = job
|
||||
return job
|
||||
|
||||
async def run_conversion(self, job_id: str):
|
||||
job = self.jobs[job_id]
|
||||
job.status = "processing"
|
||||
|
||||
input_path = self.storage_path / "uploads" / job.filename
|
||||
output_dir = self.storage_path / "outputs" / job_id
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Build command for CLI tool
|
||||
cmd = [
|
||||
"/path/to/cli-tool",
|
||||
str(input_path),
|
||||
"-o", str(output_dir),
|
||||
# Add other options...
|
||||
]
|
||||
|
||||
# Run with output capture for progress parsing
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
# Parse output for progress updates
|
||||
async def read_output(stream):
|
||||
while True:
|
||||
line = await stream.readline()
|
||||
if not line:
|
||||
break
|
||||
line_str = line.decode().strip()
|
||||
# Parse progress from CLI output
|
||||
if "%" in line_str:
|
||||
# Extract and update progress
|
||||
self.update_progress(job_id, parsed_progress)
|
||||
|
||||
await asyncio.gather(
|
||||
read_output(process.stdout),
|
||||
read_output(process.stderr)
|
||||
)
|
||||
|
||||
returncode = await process.wait()
|
||||
|
||||
if returncode == 0:
|
||||
output_files = list(output_dir.glob("*.m4b"))
|
||||
if output_files:
|
||||
job.output_file = output_files[0].name
|
||||
job.status = "completed"
|
||||
else:
|
||||
job.status = "failed"
|
||||
job.error = f"Exit code {returncode}"
|
||||
|
||||
job_manager = JobManager()
|
||||
```
|
||||
|
||||
### Backend: API Routes
|
||||
```python
|
||||
# api/routes.py
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import asyncio
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_file(file: UploadFile = File(...)):
|
||||
upload_dir = Path("/mnt/uploads")
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
file_path = upload_dir / file.filename
|
||||
|
||||
with file_path.open("wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
return {"filename": file.filename, "size": file_path.stat().st_size}
|
||||
|
||||
@router.post("/jobs")
|
||||
async def create_job(request: JobCreate):
|
||||
job = job_manager.create_job(filename=request.filename, ...)
|
||||
asyncio.create_task(job_manager.run_conversion(job.id))
|
||||
return job
|
||||
|
||||
@router.get("/jobs")
|
||||
async def list_jobs():
|
||||
return job_manager.get_all_jobs()
|
||||
|
||||
@router.get("/jobs/{job_id}/download")
|
||||
async def download_job(job_id: str):
|
||||
job = job_manager.get_job(job_id)
|
||||
if not job or job.status != "completed":
|
||||
raise HTTPException(404)
|
||||
output_path = Path("/mnt/outputs") / job_id / job.output_file
|
||||
return FileResponse(output_path, filename=job.output_file)
|
||||
```
|
||||
|
||||
### Frontend: Svelte 5 Components
|
||||
```svelte
|
||||
<!-- FileUpload.svelte -->
|
||||
<script>
|
||||
let { onUpload } = $props();
|
||||
let dragOver = $state(false);
|
||||
let uploading = $state(false);
|
||||
|
||||
async function handleUpload(file) {
|
||||
uploading = true;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
onUpload(data.filename);
|
||||
}
|
||||
uploading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dropzone"
|
||||
class:dragover={dragOver}
|
||||
ondragover={(e) => { e.preventDefault(); dragOver = true; }}
|
||||
ondragleave={() => dragOver = false}
|
||||
ondrop={(e) => { e.preventDefault(); handleUpload(e.dataTransfer.files[0]); }}>
|
||||
Drop file here
|
||||
</div>
|
||||
```
|
||||
|
||||
### Dockerfile
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Install Node for frontend build
|
||||
RUN apt-get update && apt-get install -y nodejs npm
|
||||
|
||||
# Build frontend
|
||||
COPY frontend/ /app/frontend/
|
||||
WORKDIR /app/frontend
|
||||
RUN npm install && npm run build
|
||||
|
||||
# Install backend
|
||||
COPY backend/ /app/backend/
|
||||
WORKDIR /app/backend
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# Serve static files from FastAPI
|
||||
EXPOSE 8000
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
```
|
||||
|
||||
### Terraform Deployment (GPU)
|
||||
```hcl
|
||||
resource "kubernetes_deployment" "myapp" {
|
||||
spec {
|
||||
template {
|
||||
spec {
|
||||
node_selector = { "gpu" : "true" }
|
||||
|
||||
toleration {
|
||||
key = "nvidia.com/gpu"
|
||||
operator = "Equal"
|
||||
value = "true"
|
||||
effect = "NoSchedule"
|
||||
}
|
||||
|
||||
container {
|
||||
image = "myregistry/myapp@sha256:..."
|
||||
name = "myapp"
|
||||
|
||||
resources {
|
||||
limits = { "nvidia.com/gpu" = "1" }
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "data"
|
||||
mount_path = "/mnt"
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "data"
|
||||
nfs {
|
||||
server = "10.0.10.15"
|
||||
path = "/mnt/main/myapp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
1. Upload a file via the UI
|
||||
2. Start a conversion job
|
||||
3. Watch progress update in real-time
|
||||
4. Download the completed file
|
||||
5. Verify files persist across pod restarts
|
||||
|
||||
## Notes
|
||||
- Use image digest for reliable deployments (see `k8s-docker-registry-cache-bypass` skill)
|
||||
- NFS storage persists across pod restarts
|
||||
- GPU node taints require matching tolerations
|
||||
- Consider adding job persistence (database) for production use
|
||||
- WebSocket can provide smoother progress updates than polling
|
||||
|
||||
## See Also
|
||||
- `k8s-docker-registry-cache-bypass` - Fixing image cache issues
|
||||
- `k8s-gpu-no-nvidia-devices` - GPU device troubleshooting
|
||||
- `python-filename-sanitization` - Secure file handling
|
||||
191
.claude/skills/home-assistant/SKILL.md
Normal file
191
.claude/skills/home-assistant/SKILL.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
name: home-assistant
|
||||
description: |
|
||||
Control Home Assistant smart home devices and automations. Use when:
|
||||
(1) User asks to turn on/off lights, switches, or devices,
|
||||
(2) User asks about the state of sensors, devices, or entities,
|
||||
(3) User says "turn on the lights", "set temperature", "lock the door",
|
||||
(4) User asks to run a scene or script,
|
||||
(5) User asks "what devices are on?" or "is the door locked?",
|
||||
(6) User mentions smart home, IoT, or home automation.
|
||||
Always use Home Assistant for smart home control.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2025-01-25
|
||||
---
|
||||
|
||||
# Home Assistant Control
|
||||
|
||||
## Problem
|
||||
Need to control smart home devices, check sensor states, or run automations via Home Assistant.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
- User asks to control lights, switches, covers, climate, etc.
|
||||
- User asks about device states ("is the light on?", "what's the temperature?")
|
||||
- User wants to run a scene or script
|
||||
- User mentions turning things on/off
|
||||
- User asks about smart home devices
|
||||
|
||||
## Prerequisites
|
||||
- Remote executor must be running (Python commands require remote execution)
|
||||
- The `~/.venvs/claude` virtualenv must have `requests` package installed
|
||||
- Environment variables `HOME_ASSISTANT_URL` and `HOME_ASSISTANT_TOKEN` must be set in the venv activation script
|
||||
|
||||
## Solution
|
||||
|
||||
### Script Location
|
||||
```
|
||||
/home/wizard/code/infra/.claude/home-assistant.py
|
||||
```
|
||||
|
||||
### Execution Pattern (CRITICAL)
|
||||
Always use the remote executor with venv activation to get environment variables:
|
||||
|
||||
```bash
|
||||
source ~/.venvs/claude/bin/activate && cd /home/wizard/code/infra && python .claude/home-assistant.py [command] [options]
|
||||
```
|
||||
|
||||
### Available Commands
|
||||
|
||||
#### List Entities
|
||||
```bash
|
||||
# List all entities
|
||||
python .claude/home-assistant.py list
|
||||
|
||||
# List by domain
|
||||
python .claude/home-assistant.py list --domain light
|
||||
python .claude/home-assistant.py list --domain switch
|
||||
python .claude/home-assistant.py list --domain sensor
|
||||
python .claude/home-assistant.py list --domain climate
|
||||
python .claude/home-assistant.py list --domain cover
|
||||
|
||||
# JSON output
|
||||
python .claude/home-assistant.py list --json
|
||||
```
|
||||
|
||||
#### Search Entities
|
||||
```bash
|
||||
# Search by name or ID
|
||||
python .claude/home-assistant.py search "living room"
|
||||
python .claude/home-assistant.py search "temperature"
|
||||
python .claude/home-assistant.py search "door"
|
||||
```
|
||||
|
||||
#### Get Entity State
|
||||
```bash
|
||||
python .claude/home-assistant.py state light.living_room
|
||||
python .claude/home-assistant.py state sensor.temperature
|
||||
python .claude/home-assistant.py state --json light.living_room
|
||||
```
|
||||
|
||||
#### Control Entities
|
||||
```bash
|
||||
# Turn on/off
|
||||
python .claude/home-assistant.py on light.living_room
|
||||
python .claude/home-assistant.py off switch.tv
|
||||
python .claude/home-assistant.py toggle light.bedroom
|
||||
|
||||
# Set values
|
||||
python .claude/home-assistant.py set light.living_room 75 # brightness %
|
||||
python .claude/home-assistant.py set climate.thermostat 22 # temperature
|
||||
python .claude/home-assistant.py set cover.blinds 50 # position %
|
||||
python .claude/home-assistant.py set input_number.volume 80 # numeric value
|
||||
python .claude/home-assistant.py set input_boolean.away_mode on # boolean
|
||||
python .claude/home-assistant.py set input_select.mode "Night" # select option
|
||||
```
|
||||
|
||||
#### Run Scenes and Scripts
|
||||
```bash
|
||||
# Activate a scene
|
||||
python .claude/home-assistant.py scene movie_night
|
||||
python .claude/home-assistant.py scene scene.good_morning
|
||||
|
||||
# Run a script
|
||||
python .claude/home-assistant.py script bedtime_routine
|
||||
python .claude/home-assistant.py script script.welcome_home
|
||||
```
|
||||
|
||||
#### Call Any Service
|
||||
```bash
|
||||
# Generic service call
|
||||
python .claude/home-assistant.py service light turn_on --entity light.kitchen --data '{"brightness": 255}'
|
||||
python .claude/home-assistant.py service climate set_hvac_mode --entity climate.living_room --data '{"hvac_mode": "heat"}'
|
||||
python .claude/home-assistant.py service media_player play_media --entity media_player.tv --data '{"media_content_id": "...", "media_content_type": "video"}'
|
||||
```
|
||||
|
||||
#### List Services
|
||||
```bash
|
||||
# List all available services
|
||||
python .claude/home-assistant.py services
|
||||
|
||||
# Filter by domain
|
||||
python .claude/home-assistant.py services --domain light
|
||||
python .claude/home-assistant.py services --domain climate
|
||||
```
|
||||
|
||||
#### Send Notifications
|
||||
```bash
|
||||
python .claude/home-assistant.py notify "Door left open!"
|
||||
python .claude/home-assistant.py notify "Motion detected" --title "Security Alert"
|
||||
python .claude/home-assistant.py notify "Hello" --target notify.mobile_app
|
||||
```
|
||||
|
||||
## Complete Example via Remote Executor
|
||||
|
||||
To turn on the living room light:
|
||||
|
||||
1. Write command to remote executor:
|
||||
```bash
|
||||
# Using Write tool to write to cmd_input.txt:
|
||||
source ~/.venvs/claude/bin/activate && cd /home/wizard/code/infra && python .claude/home-assistant.py on light.living_room
|
||||
```
|
||||
|
||||
2. Wait and check status:
|
||||
```bash
|
||||
sleep 2 && cat /System/Volumes/Data/mnt/wizard/code/infra/.claude/cmd_status.txt
|
||||
```
|
||||
|
||||
3. Read output:
|
||||
```bash
|
||||
cat /System/Volumes/Data/mnt/wizard/code/infra/.claude/cmd_output.txt
|
||||
```
|
||||
|
||||
## Common Entity Domains
|
||||
|
||||
| Domain | Description | Common Actions |
|
||||
|--------|-------------|----------------|
|
||||
| `light` | Lights | on, off, toggle, set brightness |
|
||||
| `switch` | Switches | on, off, toggle |
|
||||
| `sensor` | Sensors | state (read-only) |
|
||||
| `binary_sensor` | Binary sensors | state (read-only) |
|
||||
| `climate` | Thermostats | set temperature, set mode |
|
||||
| `cover` | Blinds/covers | open, close, set position |
|
||||
| `lock` | Locks | lock, unlock |
|
||||
| `media_player` | Media devices | play, pause, volume |
|
||||
| `input_boolean` | Helper toggles | on, off |
|
||||
| `input_number` | Helper numbers | set value |
|
||||
| `input_select` | Helper dropdowns | select option |
|
||||
| `script` | Scripts | run |
|
||||
| `scene` | Scenes | activate |
|
||||
| `automation` | Automations | trigger, on, off |
|
||||
|
||||
## Verification
|
||||
- Commands print confirmation message on success
|
||||
- Use `state` command to verify entity changed
|
||||
- Exit code 0 = success, 1 = error
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `HOME_ASSISTANT_URL and HOME_ASSISTANT_TOKEN must be set` | Didn't source venv activation | Use `source ~/.venvs/claude/bin/activate && python ...` |
|
||||
| `404 Not Found` | Entity doesn't exist | Use `search` command to find correct entity ID |
|
||||
| `401 Unauthorized` | Token invalid/expired | Generate new long-lived token in HA |
|
||||
| `Connection refused` | HA not reachable | Check URL and network connectivity |
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Entity IDs are case-sensitive** - use `search` to find exact IDs
|
||||
2. **Token must have sufficient permissions** - ensure token has access to all entities
|
||||
3. **Some entities require specific data** - use `services` command to see required fields
|
||||
4. **HA URL**: `https://ha-london.viktorbarzin.me`
|
||||
110
.claude/skills/k8s-docker-registry-cache-bypass/SKILL.md
Normal file
110
.claude/skills/k8s-docker-registry-cache-bypass/SKILL.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
name: k8s-docker-registry-cache-bypass
|
||||
description: |
|
||||
Fix for Kubernetes pods running old Docker images despite pushing new versions.
|
||||
Use when: (1) kubectl shows correct image tag but container runs old code,
|
||||
(2) Local registry mirror caches stale images, (3) imagePullPolicy: Always
|
||||
doesn't force fresh pulls, (4) containerd config has mirror that intercepts pulls.
|
||||
Solution: Use image digest instead of tag to bypass cache entirely.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2025-01-31
|
||||
---
|
||||
|
||||
# Kubernetes Docker Registry Cache Bypass
|
||||
|
||||
## Problem
|
||||
Kubernetes pods continue running old Docker images even after pushing new versions with
|
||||
the same tag (e.g., `:latest`). This happens when a local registry mirror caches images
|
||||
and serves stale versions, ignoring `imagePullPolicy: Always`.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
- Pod is running but application code is outdated
|
||||
- `docker push` succeeded with new layers
|
||||
- `kubectl describe pod` shows correct image tag
|
||||
- Cluster has a local registry mirror configured (e.g., in containerd config)
|
||||
- `imagePullPolicy: Always` doesn't fix the issue
|
||||
- Nodes configured with registry mirrors at `/etc/containerd/certs.d/` or similar
|
||||
|
||||
## Solution
|
||||
|
||||
### 1. Get the image digest after pushing
|
||||
```bash
|
||||
docker push viktorbarzin/myimage:latest
|
||||
# Output includes: latest: digest: sha256:abc123... size: 856
|
||||
```
|
||||
|
||||
### 2. Use digest instead of tag in deployment
|
||||
```hcl
|
||||
# Terraform
|
||||
container {
|
||||
# Use digest to bypass local registry cache
|
||||
image = "docker.io/viktorbarzin/myimage@sha256:abc123..."
|
||||
image_pull_policy = "Always"
|
||||
name = "myimage"
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Kubernetes YAML
|
||||
containers:
|
||||
- name: myimage
|
||||
image: docker.io/viktorbarzin/myimage@sha256:abc123...
|
||||
imagePullPolicy: Always
|
||||
```
|
||||
|
||||
### 3. Apply and restart
|
||||
```bash
|
||||
terraform apply -target=module.kubernetes_cluster.module.myservice
|
||||
kubectl rollout restart deployment/myservice -n mynamespace
|
||||
```
|
||||
|
||||
## Why This Works
|
||||
- Registry mirrors match by tag, not digest
|
||||
- When you specify a digest, the node must fetch that exact manifest
|
||||
- The mirror may not have the digest cached, forcing a pull from upstream
|
||||
- Even if cached, the digest guarantees the exact image version
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
# Check the pod is using the new image
|
||||
kubectl get pod -n mynamespace -o jsonpath='{.items[*].spec.containers[*].image}'
|
||||
|
||||
# Verify application behavior reflects new code
|
||||
kubectl exec -n mynamespace deploy/myservice -- <verification-command>
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Before (problematic):
|
||||
```hcl
|
||||
image = "docker.io/viktorbarzin/audiblez-web:latest"
|
||||
```
|
||||
|
||||
After (fixed):
|
||||
```hcl
|
||||
image = "docker.io/viktorbarzin/audiblez-web@sha256:4d0e2c839555e2229bc91a0b1273569bac88529e8b3c3cadad3c3cf9d865fa29"
|
||||
```
|
||||
|
||||
## Notes
|
||||
- You must update the digest each time you push a new image
|
||||
- Consider automating digest extraction in CI/CD pipelines
|
||||
- This is a workaround; ideally fix the registry mirror configuration
|
||||
- To find your registry mirror config: `cat /etc/containerd/config.toml` on nodes
|
||||
- Common mirror locations: `/etc/containerd/certs.d/docker.io/hosts.toml`
|
||||
|
||||
## Diagnosing Registry Mirror Issues
|
||||
```bash
|
||||
# On a k8s node, check containerd config
|
||||
cat /etc/containerd/config.toml | grep -A5 mirrors
|
||||
|
||||
# Check if mirror is intercepting
|
||||
crictl pull docker.io/library/alpine:latest --debug 2>&1 | grep -i mirror
|
||||
|
||||
# List cached images on node
|
||||
crictl images | grep myimage
|
||||
```
|
||||
|
||||
## References
|
||||
- [Kubernetes imagePullPolicy documentation](https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy)
|
||||
- [containerd registry configuration](https://github.com/containerd/containerd/blob/main/docs/hosts.md)
|
||||
147
.claude/skills/k8s-gpu-no-nvidia-devices/SKILL.md
Normal file
147
.claude/skills/k8s-gpu-no-nvidia-devices/SKILL.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
---
|
||||
name: k8s-gpu-no-nvidia-devices
|
||||
description: |
|
||||
Fix for Kubernetes GPU pods showing "CUDA not supported" or no /dev/nvidia* devices
|
||||
despite nvidia.com/gpu resource allocation. Use when: (1) container runs but torch.cuda.is_available()
|
||||
returns False, (2) ls /dev/nvidia* shows "no matches found", (3) nvidia-smi fails inside pod
|
||||
but works on host, (4) PyTorch/TensorFlow falls back to CPU despite GPU allocation.
|
||||
Covers NVIDIA device plugin, time-slicing, and container runtime issues.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2026-01-27
|
||||
---
|
||||
|
||||
# Kubernetes GPU Pod - No NVIDIA Devices Found
|
||||
|
||||
## Problem
|
||||
|
||||
A Kubernetes pod requests GPU resources (`nvidia.com/gpu: 1`) and schedules on a GPU node,
|
||||
but inside the container there are no NVIDIA devices visible. The application falls back
|
||||
to CPU with messages like "CUDA not supported by the Torch installed!" despite running
|
||||
in a CUDA-enabled container image.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
|
||||
- Pod shows `Running` status and is on a node with `gpu=true` label
|
||||
- `kubectl describe pod` shows GPU limit/request is satisfied
|
||||
- Inside container: `ls /dev/nvidia*` returns "no matches found"
|
||||
- Inside container: `nvidia-smi` fails or command not found
|
||||
- Application logs show: "CUDA not supported", "Switching to CPU", "torch.cuda.is_available() = False"
|
||||
- On the host node: `nvidia-smi` works fine
|
||||
|
||||
## Solution
|
||||
|
||||
### Step 1: Verify GPU Availability
|
||||
|
||||
Check if other pods are consuming the GPU:
|
||||
|
||||
```bash
|
||||
# List all pods using GPU resources
|
||||
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.containers[].resources.limits."nvidia.com/gpu" != null) | "\(.metadata.namespace)/\(.metadata.name)"'
|
||||
|
||||
# Check NVIDIA device plugin pods
|
||||
kubectl get pods -n nvidia -l app=nvidia-device-plugin
|
||||
kubectl logs -n nvidia -l app=nvidia-device-plugin --tail=50
|
||||
```
|
||||
|
||||
### Step 2: Free GPU Resources
|
||||
|
||||
If another workload is using the GPU, unload it:
|
||||
|
||||
```bash
|
||||
# For Ollama specifically
|
||||
kubectl exec -n ollama deployment/ollama -- ollama stop <model_name>
|
||||
|
||||
# Or scale down the conflicting deployment
|
||||
kubectl scale deployment/<name> -n <namespace> --replicas=0
|
||||
```
|
||||
|
||||
### Step 3: Restart the Affected Pod
|
||||
|
||||
After freeing GPU resources, restart the pod to get fresh device allocation:
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment/<name> -n <namespace>
|
||||
|
||||
# Or delete the pod directly
|
||||
kubectl delete pod <pod-name> -n <namespace>
|
||||
```
|
||||
|
||||
### Step 4: Verify GPU Access
|
||||
|
||||
```bash
|
||||
# Check devices are now visible
|
||||
kubectl exec -n <namespace> deployment/<name> -- ls -la /dev/nvidia*
|
||||
|
||||
# Test nvidia-smi
|
||||
kubectl exec -n <namespace> deployment/<name> -- nvidia-smi
|
||||
|
||||
# Test PyTorch CUDA
|
||||
kubectl exec -n <namespace> deployment/<name> -- python3 -c "import torch; print('CUDA:', torch.cuda.is_available())"
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After restart, you should see:
|
||||
|
||||
```
|
||||
/dev/nvidia0
|
||||
/dev/nvidiactl
|
||||
/dev/nvidia-uvm
|
||||
/dev/nvidia-uvm-tools
|
||||
```
|
||||
|
||||
And `nvidia-smi` should show the GPU with your container process.
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
# Problem: ebook2audiobook shows "CUDA not supported"
|
||||
$ kubectl exec -n ebook2audiobook deployment/ebook2audiobook -- ls /dev/nvidia*
|
||||
zsh:1: no matches found: /dev/nvidia*
|
||||
|
||||
# Solution: Unload Ollama model holding the GPU
|
||||
$ kubectl exec -n ollama deployment/ollama -- ollama ps
|
||||
NAME SIZE PROCESSOR
|
||||
qwen2.5:14b 10 GB 33%/67% CPU/GPU
|
||||
|
||||
$ kubectl exec -n ollama deployment/ollama -- ollama stop qwen2.5:14b
|
||||
|
||||
# Restart the affected pod
|
||||
$ kubectl rollout restart deployment/ebook2audiobook -n ebook2audiobook
|
||||
|
||||
# Verify
|
||||
$ kubectl exec -n ebook2audiobook deployment/ebook2audiobook -- nvidia-smi
|
||||
# Should now show the Tesla T4 GPU
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- **GPU Time-Slicing**: If using NVIDIA GPU time-slicing (configured in GPU Operator),
|
||||
multiple pods can share a GPU. However, device injection still requires proper timing.
|
||||
|
||||
- **Pod Scheduling Order**: Pods that start while GPU is fully allocated may not get
|
||||
devices injected even after GPU becomes available - a restart is required.
|
||||
|
||||
- **Container Runtime**: The NVIDIA Container Toolkit must be properly configured.
|
||||
Issues can arise from:
|
||||
- cgroup driver mismatch (systemd vs cgroupfs)
|
||||
- Container updates causing device loss
|
||||
- SELinux blocking device access
|
||||
|
||||
- **Image Compatibility**: The container image must have CUDA libraries matching the
|
||||
driver version. Check with `nvidia-smi` on host for driver version.
|
||||
|
||||
- **This Cluster**: Uses NVIDIA GPU Operator with time-slicing (20 replicas per GPU).
|
||||
GPU node is `k8s-node1` with Tesla T4.
|
||||
|
||||
## See Also
|
||||
|
||||
- Check GPU Operator status: `kubectl get pods -n nvidia`
|
||||
- View time-slicing config: `kubectl get configmap -n nvidia time-slicing-config -o yaml`
|
||||
|
||||
## References
|
||||
|
||||
- [NVIDIA Container Toolkit Troubleshooting](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/troubleshooting.html)
|
||||
- [Kubernetes GPU Device Plugin](https://github.com/NVIDIA/k8s-device-plugin)
|
||||
- [NVIDIA GPU Operator](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/overview.html)
|
||||
96
.claude/skills/k8s-nfs-mount-troubleshooting/SKILL.md
Normal file
96
.claude/skills/k8s-nfs-mount-troubleshooting/SKILL.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
name: k8s-nfs-mount-troubleshooting
|
||||
description: |
|
||||
Debug Kubernetes NFS volume mount failures. Use when: (1) Pod stuck in ContainerCreating
|
||||
for extended time, (2) kubectl describe shows "MountVolume.SetUp failed" with NFS errors,
|
||||
(3) Error message shows "Protocol not supported" or "mount.nfs: access denied",
|
||||
(4) NFS volume defined in pod spec but container won't start. Common root cause is
|
||||
missing NFS export on the server, not a protocol issue.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2026-01-28
|
||||
---
|
||||
|
||||
# Kubernetes NFS Mount Troubleshooting
|
||||
|
||||
## Problem
|
||||
Pods with NFS volumes get stuck in `ContainerCreating` state indefinitely. The error
|
||||
messages from `kubectl describe pod` can be misleading, showing protocol or permission
|
||||
errors when the actual issue is the NFS export doesn't exist.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
- Pod status shows `ContainerCreating` for more than 1-2 minutes
|
||||
- `kubectl describe pod` shows events like:
|
||||
- `MountVolume.SetUp failed for volume "data" : mount failed: exit status 32`
|
||||
- `mount.nfs: Protocol not supported`
|
||||
- `mount.nfs: access denied by server`
|
||||
- Pod spec includes an NFS volume mount
|
||||
- Other pods on the same node work fine
|
||||
|
||||
## Solution
|
||||
|
||||
### Step 1: Identify the NFS path
|
||||
```bash
|
||||
kubectl describe pod -n <namespace> <pod-name> | grep -A5 "Volumes:"
|
||||
```
|
||||
Look for the NFS server and path (e.g., `10.0.10.15:/mnt/main/myservice`)
|
||||
|
||||
### Step 2: Verify the export exists on NFS server
|
||||
SSH to the NFS server and check:
|
||||
```bash
|
||||
ssh root@<nfs-server> "ls -la /mnt/main/myservice"
|
||||
```
|
||||
|
||||
### Step 3: If directory doesn't exist, create it
|
||||
```bash
|
||||
ssh root@<nfs-server> "mkdir -p /mnt/main/myservice && chmod 777 /mnt/main/myservice"
|
||||
```
|
||||
|
||||
### Step 4: Add to NFS exports (TrueNAS specific)
|
||||
For TrueNAS, add the path to the NFS share configuration:
|
||||
1. Add directory to `scripts/nfs_directories.txt`
|
||||
2. Run `scripts/nfs_exports.sh` to update the share via API
|
||||
|
||||
### Step 5: Restart the pod
|
||||
```bash
|
||||
kubectl delete pod -n <namespace> -l app=<app-label>
|
||||
```
|
||||
The deployment will create a new pod that should now mount successfully.
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
kubectl get pods -n <namespace>
|
||||
# Should show 1/1 Running instead of 0/1 ContainerCreating
|
||||
|
||||
kubectl exec -n <namespace> <pod-name> -- ls -la /app/data
|
||||
# Should show the mounted directory contents
|
||||
```
|
||||
|
||||
## Example
|
||||
**Symptom:**
|
||||
```
|
||||
Events:
|
||||
Warning FailedMount 55s (x13 over 11m) kubelet MountVolume.SetUp failed for volume "data" : mount failed: exit status 32
|
||||
Mounting command: mount
|
||||
Mounting arguments: -t nfs 10.0.10.15:/mnt/main/resume /var/lib/kubelet/pods/.../data
|
||||
Output: mount.nfs: Protocol not supported
|
||||
```
|
||||
|
||||
**Root Cause:** The directory `/mnt/main/resume` didn't exist on the TrueNAS server.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
ssh root@10.0.10.15 'mkdir -p /mnt/main/resume && chmod 777 /mnt/main/resume'
|
||||
# Then add to NFS exports and restart pod
|
||||
```
|
||||
|
||||
## Notes
|
||||
- The "Protocol not supported" error is misleading - it often means the export path doesn't exist
|
||||
- Always check the NFS server first before investigating protocol/firewall issues
|
||||
- For TrueNAS, the NFS share must be updated via API/UI after creating new directories
|
||||
- NFSv3 vs NFSv4 issues are rare in modern setups; missing paths are more common
|
||||
- Check that the NFS client packages are installed on Kubernetes nodes if this is a new cluster
|
||||
|
||||
## See Also
|
||||
- TrueNAS NFS configuration documentation
|
||||
- Kubernetes NFS volume documentation
|
||||
142
.claude/skills/nextcloud-calendar/SKILL.md
Normal file
142
.claude/skills/nextcloud-calendar/SKILL.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
---
|
||||
name: nextcloud-calendar
|
||||
description: |
|
||||
Create, list, and query calendar events in Nextcloud via CalDAV. Use when:
|
||||
(1) User asks to create a calendar event, (2) User asks what's on their calendar,
|
||||
(3) User says "add to calendar" or "schedule", (4) User asks about upcoming events.
|
||||
Always use Nextcloud calendar unless user specifies otherwise.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2025-01-25
|
||||
---
|
||||
|
||||
# Nextcloud Calendar Management
|
||||
|
||||
## Problem
|
||||
Need to create, query, or manage calendar events in the user's Nextcloud calendar.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
- User asks to create/add a calendar event
|
||||
- User asks "what's on my calendar?" or similar
|
||||
- User mentions scheduling something
|
||||
- User says "remind me" with a date (create calendar event)
|
||||
- Default calendar is always Nextcloud unless otherwise specified
|
||||
|
||||
## Prerequisites
|
||||
- Remote executor must be running (Python commands require remote execution)
|
||||
- The `~/.venvs/claude` virtualenv must have `caldav` and `icalendar` packages installed
|
||||
- Environment variables `NEXTCLOUD_USER` and `NEXTCLOUD_APP_PASSWORD` must be set in the venv activation script
|
||||
|
||||
## Solution
|
||||
|
||||
### Script Location
|
||||
```
|
||||
/home/wizard/code/infra/.claude/calendar-query.py
|
||||
```
|
||||
|
||||
### Execution Pattern (CRITICAL)
|
||||
Always use the remote executor with venv activation to get environment variables:
|
||||
|
||||
```bash
|
||||
source ~/.venvs/claude/bin/activate && cd /home/wizard/code/infra && python .claude/calendar-query.py [command] [options]
|
||||
```
|
||||
|
||||
### Available Commands
|
||||
|
||||
#### List Calendars
|
||||
```bash
|
||||
python .claude/calendar-query.py list
|
||||
```
|
||||
|
||||
#### Query Events
|
||||
```bash
|
||||
# Today's events
|
||||
python .claude/calendar-query.py today
|
||||
|
||||
# Tomorrow's events
|
||||
python .claude/calendar-query.py tomorrow
|
||||
|
||||
# This week
|
||||
python .claude/calendar-query.py week
|
||||
|
||||
# This month
|
||||
python .claude/calendar-query.py month
|
||||
|
||||
# Custom date range
|
||||
python .claude/calendar-query.py events --days 14
|
||||
python .claude/calendar-query.py events --date 2026-04-10
|
||||
|
||||
# From specific calendar
|
||||
python .claude/calendar-query.py today --calendar "Work"
|
||||
```
|
||||
|
||||
#### Create Events
|
||||
```bash
|
||||
# All-day event (single day)
|
||||
python .claude/calendar-query.py create --title "Doctor appointment" --start "2026-03-15" --all-day
|
||||
|
||||
# All-day event (multi-day) - end date is EXCLUSIVE
|
||||
# For April 10-13, use end date April 14
|
||||
python .claude/calendar-query.py create --title "Vacation" --start "2026-04-10" --end "2026-04-14" --all-day
|
||||
|
||||
# Timed event
|
||||
python .claude/calendar-query.py create --title "Meeting" --start "2026-03-15 14:00" --end "2026-03-15 15:00"
|
||||
|
||||
# With location and description
|
||||
python .claude/calendar-query.py create --title "Lunch" --start "tomorrow 12:00" --location "Cafe" --description "Team lunch"
|
||||
|
||||
# Relative dates work
|
||||
python .claude/calendar-query.py create --title "Call" --start "today 16:00"
|
||||
python .claude/calendar-query.py create --title "Review" --start "tomorrow 10:00"
|
||||
```
|
||||
|
||||
### Output Formats
|
||||
```bash
|
||||
# JSON output (for parsing)
|
||||
python .claude/calendar-query.py today --json
|
||||
|
||||
# Text output (default, human-readable)
|
||||
python .claude/calendar-query.py week
|
||||
```
|
||||
|
||||
## Complete Example via Remote Executor
|
||||
|
||||
To create an event "Team offsite" from March 20-22, 2026:
|
||||
|
||||
1. Write command to remote executor:
|
||||
```bash
|
||||
echo 'source ~/.venvs/claude/bin/activate && cd /home/wizard/code/infra && python .claude/calendar-query.py create --title "Team offsite" --start "2026-03-20" --end "2026-03-23" --all-day' > /System/Volumes/Data/mnt/wizard/code/infra/.claude/cmd_input.txt
|
||||
```
|
||||
|
||||
2. Wait and check status:
|
||||
```bash
|
||||
sleep 3 && cat /System/Volumes/Data/mnt/wizard/code/infra/.claude/cmd_status.txt
|
||||
```
|
||||
|
||||
3. Read output:
|
||||
```bash
|
||||
cat /System/Volumes/Data/mnt/wizard/code/infra/.claude/cmd_output.txt
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **End dates are exclusive** for all-day events (CalDAV standard). To create an event spanning April 10-13, set end to April 14.
|
||||
|
||||
2. **Must source venv activation** - Using `~/.venvs/claude/bin/python` directly won't work because environment variables (`NEXTCLOUD_USER`, `NEXTCLOUD_APP_PASSWORD`) are set in the activation script.
|
||||
|
||||
3. **No delete/update commands** - The script currently only supports create and query. To modify events, user must do it manually in Nextcloud.
|
||||
|
||||
4. **Default calendar** is "Personal" - use `--calendar` flag for others.
|
||||
|
||||
## Verification
|
||||
- For queries: Output shows formatted event list
|
||||
- For creates: Output shows "Event created: [title]" with calendar name and start date
|
||||
- Exit code 0 = success, 1 = error (check output for details)
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `NEXTCLOUD_USER and NEXTCLOUD_APP_PASSWORD must be set` | Didn't source venv activation | Use `source ~/.venvs/claude/bin/activate && python ...` |
|
||||
| `Required packages not installed` | caldav/icalendar missing | Run `~/.venvs/claude/bin/pip install caldav icalendar` |
|
||||
| `Calendar 'X' not found` | Wrong calendar name | Run `list` command to see available calendars |
|
||||
182
.claude/skills/python-filename-sanitization/SKILL.md
Normal file
182
.claude/skills/python-filename-sanitization/SKILL.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
---
|
||||
name: python-filename-sanitization
|
||||
description: |
|
||||
Secure filename sanitization pattern for Python web applications. Use when:
|
||||
(1) Accepting user-provided filenames for file operations, (2) Building file
|
||||
rename/upload functionality, (3) Preventing path traversal attacks (../../../etc/passwd),
|
||||
(4) Preventing shell injection through filenames, (5) FastAPI/Flask file handling.
|
||||
Provides regex-based whitelist approach with pathlib for safe file operations.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2025-01-31
|
||||
---
|
||||
|
||||
# Python Filename Sanitization
|
||||
|
||||
## Problem
|
||||
User-provided filenames can contain malicious characters that enable path traversal
|
||||
attacks, shell injection, or filesystem corruption. Direct use of user input in
|
||||
file paths is a security vulnerability.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
- Building file upload, rename, or download functionality
|
||||
- User can specify filenames via API or form input
|
||||
- Files are stored on server filesystem
|
||||
- Need to prevent: `../`, shell metacharacters, null bytes, etc.
|
||||
|
||||
## Solution
|
||||
|
||||
### Complete Sanitization Function
|
||||
```python
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def sanitize_filename(filename: str, max_length: int = 200) -> str:
|
||||
"""
|
||||
Sanitize a filename to prevent path traversal and shell injection.
|
||||
Only allows alphanumeric characters, spaces, hyphens, underscores,
|
||||
parentheses, and dots.
|
||||
"""
|
||||
if not filename:
|
||||
raise ValueError("Filename cannot be empty")
|
||||
|
||||
# Remove any path components (prevent path traversal)
|
||||
filename = Path(filename).name
|
||||
|
||||
# Only allow safe characters: alphanumeric, space, hyphen, underscore, parentheses, dot
|
||||
# This regex removes anything that isn't in the allowed set
|
||||
safe_filename = re.sub(r'[^a-zA-Z0-9\s\-_().]', '', filename)
|
||||
|
||||
# Collapse multiple spaces/dots
|
||||
safe_filename = re.sub(r'\s+', ' ', safe_filename)
|
||||
safe_filename = re.sub(r'\.+', '.', safe_filename)
|
||||
|
||||
# Strip leading/trailing whitespace and dots
|
||||
safe_filename = safe_filename.strip(' .')
|
||||
|
||||
# Limit length
|
||||
if len(safe_filename) > max_length:
|
||||
safe_filename = safe_filename[:max_length]
|
||||
|
||||
if not safe_filename:
|
||||
raise ValueError("Filename contains no valid characters")
|
||||
|
||||
return safe_filename
|
||||
```
|
||||
|
||||
### FastAPI Integration Example
|
||||
```python
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from pathlib import Path
|
||||
|
||||
class RenameRequest(BaseModel):
|
||||
new_name: str
|
||||
|
||||
@router.patch("/files/{file_id}/rename")
|
||||
async def rename_file(file_id: str, request: RenameRequest):
|
||||
"""Rename a file with sanitized input."""
|
||||
file_dir = Path("/data/files") / file_id
|
||||
|
||||
if not file_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Find existing file
|
||||
files = list(file_dir.glob("*"))
|
||||
if not files:
|
||||
raise HTTPException(status_code=404, detail="No file found")
|
||||
|
||||
current_file = files[0]
|
||||
current_extension = current_file.suffix
|
||||
|
||||
# Sanitize the new name
|
||||
try:
|
||||
safe_name = sanitize_filename(request.new_name)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# Preserve original extension
|
||||
if not safe_name.lower().endswith(current_extension.lower()):
|
||||
safe_name = safe_name + current_extension
|
||||
|
||||
# Create new path (same directory, new filename)
|
||||
new_file = file_dir / safe_name
|
||||
|
||||
# Check for conflicts
|
||||
if new_file.exists() and new_file != current_file:
|
||||
raise HTTPException(status_code=400, detail="A file with that name already exists")
|
||||
|
||||
# Rename using pathlib (no shell commands!)
|
||||
current_file.rename(new_file)
|
||||
|
||||
return {"status": "renamed", "new_filename": safe_name}
|
||||
```
|
||||
|
||||
## Key Security Principles
|
||||
|
||||
### 1. Whitelist, Don't Blacklist
|
||||
```python
|
||||
# BAD: Trying to block dangerous characters
|
||||
filename = filename.replace('../', '').replace('\x00', '')
|
||||
|
||||
# GOOD: Only allow known-safe characters
|
||||
safe_filename = re.sub(r'[^a-zA-Z0-9\s\-_().]', '', filename)
|
||||
```
|
||||
|
||||
### 2. Use pathlib, Not Shell Commands
|
||||
```python
|
||||
# BAD: Shell command (vulnerable to injection)
|
||||
os.system(f'mv "{old_path}" "{new_path}"')
|
||||
|
||||
# GOOD: Pure Python (no shell)
|
||||
old_path.rename(new_path)
|
||||
```
|
||||
|
||||
### 3. Extract Basename First
|
||||
```python
|
||||
# BAD: User could submit "../../../etc/passwd"
|
||||
filename = user_input
|
||||
|
||||
# GOOD: Extract just the filename part
|
||||
filename = Path(user_input).name
|
||||
```
|
||||
|
||||
### 4. Validate After Sanitization
|
||||
```python
|
||||
# Ensure something remains after sanitization
|
||||
if not safe_filename:
|
||||
raise ValueError("Filename contains no valid characters")
|
||||
```
|
||||
|
||||
## Verification
|
||||
```python
|
||||
# Test cases that should be handled safely
|
||||
assert sanitize_filename("normal.txt") == "normal.txt"
|
||||
assert sanitize_filename("../../../etc/passwd") == "etcpasswd"
|
||||
assert sanitize_filename("file; rm -rf /") == "file rm -rf"
|
||||
assert sanitize_filename(" spaces .txt") == "spaces.txt"
|
||||
assert sanitize_filename("$(whoami).txt") == "whoami.txt"
|
||||
|
||||
# Test cases that should raise errors
|
||||
try:
|
||||
sanitize_filename("") # Should raise ValueError
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
sanitize_filename("$#@!") # Should raise ValueError (no valid chars)
|
||||
except ValueError:
|
||||
pass
|
||||
```
|
||||
|
||||
## Notes
|
||||
- This is intentionally restrictive; expand the regex if you need Unicode support
|
||||
- For Unicode filenames, consider `unicodedata.normalize('NFKD', ...)` first
|
||||
- Max length of 200 is conservative; filesystem limits vary (255 bytes typical)
|
||||
- Always preserve file extensions when renaming to avoid breaking file associations
|
||||
- Consider adding a UUID prefix for guaranteed uniqueness in upload scenarios
|
||||
|
||||
## References
|
||||
- [OWASP Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal)
|
||||
- [CWE-22: Path Traversal](https://cwe.mitre.org/data/definitions/22.html)
|
||||
- [Python pathlib documentation](https://docs.python.org/3/library/pathlib.html)
|
||||
388
.claude/skills/setup-project.md
Normal file
388
.claude/skills/setup-project.md
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
# Setup Project Skill
|
||||
|
||||
**Purpose**: Deploy a new self-hosted service to the Kubernetes cluster from a GitHub repository.
|
||||
|
||||
**When to use**: User provides a GitHub URL or project name and wants to deploy it to the cluster.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Research Phase
|
||||
|
||||
**Input**: GitHub repository URL or project name
|
||||
|
||||
**Actions**:
|
||||
- Visit the GitHub repository
|
||||
- Check the README for:
|
||||
- Official Docker image (Docker Hub, ghcr.io, etc.)
|
||||
- docker-compose.yml file
|
||||
- Self-hosting documentation
|
||||
- Required dependencies (PostgreSQL, MySQL, Redis, etc.)
|
||||
- Environment variables needed
|
||||
- Default ports
|
||||
- Storage requirements
|
||||
|
||||
**Find Docker Image Priority**:
|
||||
1. Check official documentation for recommended image
|
||||
2. Look in docker-compose.yml for `image:` directive
|
||||
3. Check GitHub Container Registry: `ghcr.io/<org>/<repo>`
|
||||
4. Check Docker Hub: `<org>/<repo>`
|
||||
5. Check releases page for container images
|
||||
6. Last resort: Build from Dockerfile (avoid if possible)
|
||||
|
||||
**Extract Configuration**:
|
||||
- Container port (default port the app listens on)
|
||||
- Environment variables (DATABASE_URL, REDIS_HOST, SMTP, etc.)
|
||||
- Volume mounts (what data needs persistence)
|
||||
- Dependencies (database type, cache, etc.)
|
||||
|
||||
### 2. Database Setup (if needed)
|
||||
|
||||
**If project requires PostgreSQL**:
|
||||
- User provides database credentials or use pattern: `<service>` user with secure password
|
||||
- Database will be created in shared `postgresql.dbaas.svc.cluster.local`
|
||||
- Connection string format: `postgresql://<user>:<password>@postgresql.dbaas.svc.cluster.local:5432/<dbname>`
|
||||
|
||||
**If project requires MySQL**:
|
||||
- User provides database credentials
|
||||
- Database in shared `mysql.dbaas.svc.cluster.local`
|
||||
- Connection string format: `mysql://<user>:<password>@mysql.dbaas.svc.cluster.local:3306/<dbname>`
|
||||
|
||||
**If project requires Redis**:
|
||||
- Use shared Redis: `redis.redis.svc.cluster.local:6379`
|
||||
- No password required
|
||||
|
||||
**IMPORTANT**: Never create databases yourself - always ask user for credentials to use.
|
||||
|
||||
### 3. Terraform Module Creation
|
||||
|
||||
**Create module directory**:
|
||||
```bash
|
||||
mkdir -p modules/kubernetes/<service-name>/
|
||||
```
|
||||
|
||||
**Create `modules/kubernetes/<service-name>/main.tf`**:
|
||||
|
||||
```hcl
|
||||
variable "tls_secret_name" {}
|
||||
variable "tier" { type = string }
|
||||
variable "postgresql_password" {} # Only if needed
|
||||
# Add other variables as needed (smtp_password, api_keys, etc.)
|
||||
|
||||
resource "kubernetes_namespace" "<service>" {
|
||||
metadata {
|
||||
name = "<service>"
|
||||
}
|
||||
}
|
||||
|
||||
module "tls_secret" {
|
||||
source = "../setup_tls_secret"
|
||||
namespace = kubernetes_namespace.<service>.metadata[0].name
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
|
||||
# If database migrations needed, add init_container
|
||||
resource "kubernetes_deployment" "<service>" {
|
||||
metadata {
|
||||
name = "<service>"
|
||||
namespace = kubernetes_namespace.<service>.metadata[0].name
|
||||
labels = {
|
||||
app = "<service>"
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
spec {
|
||||
replicas = 1
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "<service>"
|
||||
}
|
||||
}
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "<service>"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
# Init container for migrations (if needed)
|
||||
# init_container { ... }
|
||||
|
||||
container {
|
||||
name = "<service>"
|
||||
image = "<docker-image>:<tag>"
|
||||
|
||||
port {
|
||||
container_port = <port>
|
||||
}
|
||||
|
||||
# Environment variables
|
||||
env {
|
||||
name = "DATABASE_URL"
|
||||
value = "postgresql://<service>:${var.postgresql_password}@postgresql.dbaas.svc.cluster.local:5432/<service>"
|
||||
}
|
||||
# Add other env vars as needed
|
||||
|
||||
# Volume mounts for persistent data
|
||||
volume_mount {
|
||||
name = "data"
|
||||
mount_path = "<mount-path>"
|
||||
sub_path = "<optional-subpath>"
|
||||
}
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
memory = "256Mi"
|
||||
cpu = "100m"
|
||||
}
|
||||
limits = {
|
||||
memory = "2Gi"
|
||||
cpu = "1"
|
||||
}
|
||||
}
|
||||
|
||||
# Health checks (if endpoints exist)
|
||||
liveness_probe {
|
||||
http_get {
|
||||
path = "/health" # or /healthz, /, etc.
|
||||
port = <port>
|
||||
}
|
||||
initial_delay_seconds = 60
|
||||
period_seconds = 30
|
||||
}
|
||||
}
|
||||
|
||||
# NFS volume for persistence
|
||||
volume {
|
||||
name = "data"
|
||||
nfs {
|
||||
server = "10.0.10.15"
|
||||
path = "/mnt/main/<service>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service" "<service>" {
|
||||
metadata {
|
||||
name = "<service>"
|
||||
namespace = kubernetes_namespace.<service>.metadata[0].name
|
||||
labels = {
|
||||
app = "<service>"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
selector = {
|
||||
app = "<service>"
|
||||
}
|
||||
port {
|
||||
name = "http"
|
||||
port = 80
|
||||
target_port = <container-port>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "ingress" {
|
||||
source = "../ingress_factory"
|
||||
namespace = kubernetes_namespace.<service>.metadata[0].name
|
||||
name = "<service>"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
# Add extra_annotations if needed (proxy-body-size, timeouts, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Main Terraform Files
|
||||
|
||||
**Add to `modules/kubernetes/main.tf`**:
|
||||
|
||||
1. Add variable declarations at top:
|
||||
```hcl
|
||||
variable "<service>_postgresql_password" { type = string }
|
||||
```
|
||||
|
||||
2. Add to appropriate DEFCON level (ask user which level, default to 5):
|
||||
```hcl
|
||||
5 : [
|
||||
...,
|
||||
"<service>"
|
||||
]
|
||||
```
|
||||
|
||||
3. Add module block at bottom:
|
||||
```hcl
|
||||
module "<service>" {
|
||||
source = "./<service>"
|
||||
for_each = contains(local.active_modules, "<service>") ? { <service> = true } : {}
|
||||
tls_secret_name = var.tls_secret_name
|
||||
postgresql_password = var.<service>_postgresql_password
|
||||
tier = local.tiers.aux # or appropriate tier
|
||||
|
||||
depends_on = [null_resource.core_services]
|
||||
}
|
||||
```
|
||||
|
||||
**Add to `main.tf`**:
|
||||
|
||||
1. Add variable:
|
||||
```hcl
|
||||
variable "<service>_postgresql_password" { type = string }
|
||||
```
|
||||
|
||||
2. Pass to kubernetes_cluster module:
|
||||
```hcl
|
||||
module "kubernetes_cluster" {
|
||||
...
|
||||
<service>_postgresql_password = var.<service>_postgresql_password
|
||||
}
|
||||
```
|
||||
|
||||
**Update `terraform.tfvars`**:
|
||||
|
||||
1. Add password/credentials:
|
||||
```hcl
|
||||
<service>_postgresql_password = "<secure-password>"
|
||||
```
|
||||
|
||||
2. Add to Cloudflare DNS (ask user if proxied or non-proxied):
|
||||
```hcl
|
||||
cloudflare_non_proxied_names = [
|
||||
...,
|
||||
"<service>"
|
||||
]
|
||||
```
|
||||
|
||||
### 5. Email/SMTP Configuration (if needed)
|
||||
|
||||
If service needs to send emails:
|
||||
```hcl
|
||||
env {
|
||||
name = "MAILER_HOST"
|
||||
value = "mailserver.viktorbarzin.me" # Public hostname for TLS
|
||||
}
|
||||
env {
|
||||
name = "MAILER_PORT"
|
||||
value = "587"
|
||||
}
|
||||
env {
|
||||
name = "MAILER_USER"
|
||||
value = "info@viktorbarzin.me"
|
||||
}
|
||||
env {
|
||||
name = "MAILER_PASSWORD"
|
||||
value = var.mailserver_accounts["info@viktorbarzin.me"] # Pass from module
|
||||
}
|
||||
```
|
||||
|
||||
Add to module call:
|
||||
```hcl
|
||||
smtp_password = var.mailserver_accounts["info@viktorbarzin.me"]
|
||||
```
|
||||
|
||||
### 6. Apply Terraform
|
||||
|
||||
```bash
|
||||
# Via remote executor
|
||||
terraform init
|
||||
terraform apply -target=module.kubernetes_cluster.module.<service> -auto-approve
|
||||
```
|
||||
|
||||
### 7. Verification
|
||||
|
||||
```bash
|
||||
kubectl get pods -n <service>
|
||||
kubectl logs -n <service> -l app=<service> --tail=50
|
||||
```
|
||||
|
||||
Test URL: `https://<service>.viktorbarzin.me`
|
||||
|
||||
### 8. Commit Changes
|
||||
|
||||
```bash
|
||||
git add modules/kubernetes/<service>/ main.tf modules/kubernetes/main.tf terraform.tfvars
|
||||
git commit -m "Add <service> deployment
|
||||
|
||||
- Deploy <service> as <description>
|
||||
- Uses <dependencies>
|
||||
- Ingress at <service>.viktorbarzin.me
|
||||
|
||||
[ci skip]"
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Init Container for Migrations
|
||||
```hcl
|
||||
init_container {
|
||||
name = "migration"
|
||||
image = "<same-image>"
|
||||
command = ["sh", "-c", "<migration-command>"]
|
||||
|
||||
# Same env vars and volumes as main container
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Environment Variables
|
||||
```hcl
|
||||
locals {
|
||||
common_env = [
|
||||
{ name = "VAR1", value = "value1" },
|
||||
{ name = "VAR2", value = "value2" },
|
||||
]
|
||||
}
|
||||
|
||||
dynamic "env" {
|
||||
for_each = local.common_env
|
||||
content {
|
||||
name = env.value.name
|
||||
value = env.value.value
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### External URL Configuration
|
||||
Many apps need their public URL configured:
|
||||
```hcl
|
||||
env {
|
||||
name = "APP_URL" # or PUBLIC_URL, EXTERNAL_URL, etc.
|
||||
value = "https://<service>.viktorbarzin.me"
|
||||
}
|
||||
env {
|
||||
name = "HTTPS" # or ENABLE_HTTPS, etc.
|
||||
value = "true"
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Find official Docker image or docker-compose
|
||||
- [ ] Identify dependencies (DB, Redis, etc.)
|
||||
- [ ] Ask user for database credentials (never create yourself)
|
||||
- [ ] Create `modules/kubernetes/<service>/main.tf`
|
||||
- [ ] Update `modules/kubernetes/main.tf` (variables, DEFCON level, module block)
|
||||
- [ ] Update `main.tf` (variable, pass to module)
|
||||
- [ ] Update `terraform.tfvars` (password, Cloudflare DNS)
|
||||
- [ ] Run `terraform init` and `terraform apply`
|
||||
- [ ] Verify pods are running
|
||||
- [ ] Test the URL
|
||||
- [ ] Commit changes with `[ci skip]`
|
||||
|
||||
## Questions to Ask User
|
||||
|
||||
1. What DEFCON level should this service be in? (Default: 5)
|
||||
2. Should Cloudflare proxy this domain? (Default: no, add to non_proxied_names)
|
||||
3. Does this need email/SMTP? (Configure if yes)
|
||||
4. What database credentials should I use? (Never create yourself)
|
||||
5. What tier? (core/cluster/gpu/edge/aux - default: aux)
|
||||
|
||||
## Notes
|
||||
|
||||
- **Always use official documentation** as the source of truth
|
||||
- **Prefer stable/latest tags** over specific versions for self-hosted
|
||||
- **Use shared infrastructure**: PostgreSQL at `postgresql.dbaas.svc.cluster.local`, Redis at `redis.redis.svc.cluster.local`
|
||||
- **NFS storage**: Always at `10.0.10.15:/mnt/main/<service>`
|
||||
- **Email**: Use `mailserver.viktorbarzin.me` (public hostname) not internal service name
|
||||
- **Resource limits**: Start conservative, can increase if needed
|
||||
- **Health checks**: Only add if the app has health endpoints
|
||||
102
.claude/skills/setup-remote-executor.md
Normal file
102
.claude/skills/setup-remote-executor.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Setup Shared Remote Executor
|
||||
|
||||
Skill for setting up Claude Code's shared remote executor in new projects.
|
||||
|
||||
## When to Use
|
||||
- When adding Claude Code support to a new project
|
||||
- When the user says "set up remote executor for this project"
|
||||
- When working on a new project that needs remote command execution
|
||||
|
||||
## Prerequisites
|
||||
- Shared executor already deployed at `~/.claude/` on wizard@10.0.10.10
|
||||
- Project accessible via NFS from both macOS and the remote VM
|
||||
|
||||
## Setup Steps
|
||||
|
||||
### 1. Create .claude Directory
|
||||
```bash
|
||||
mkdir -p .claude/sessions
|
||||
```
|
||||
|
||||
### 2. Create session-exec.sh Wrapper
|
||||
Create `.claude/session-exec.sh` with the following content (adjust PROJECT_ROOT):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Project-Local Session Helper - Wrapper for shared executor
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SHARED_SESSION_EXEC="/home/wizard/.claude/session-exec.sh"
|
||||
PROJECT_ROOT="/home/wizard/path/to/project" # UPDATE THIS
|
||||
|
||||
if [ -f "$SHARED_SESSION_EXEC" ]; then
|
||||
if [ "${1:-}" = "create" ] || [ -z "${1:-}" ]; then
|
||||
"$SHARED_SESSION_EXEC" create "$PROJECT_ROOT"
|
||||
else
|
||||
"$SHARED_SESSION_EXEC" "$@"
|
||||
fi
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SESSIONS_DIR="$SCRIPT_DIR/sessions"
|
||||
SESSION_ID="${1:-$(date +%s)-$$-$RANDOM}"
|
||||
ACTION="${2:-create}"
|
||||
SESSION_DIR="$SESSIONS_DIR/$SESSION_ID"
|
||||
|
||||
case "$ACTION" in
|
||||
create|init|"")
|
||||
mkdir -p "$SESSION_DIR"
|
||||
echo "ready" > "$SESSION_DIR/cmd_status.txt"
|
||||
echo "$PROJECT_ROOT" > "$SESSION_DIR/workdir.txt"
|
||||
> "$SESSION_DIR/cmd_input.txt"
|
||||
> "$SESSION_DIR/cmd_output.txt"
|
||||
echo "$SESSION_ID"
|
||||
;;
|
||||
cleanup|remove|delete)
|
||||
[ -d "$SESSION_DIR" ] && rm -rf "$SESSION_DIR"
|
||||
;;
|
||||
status)
|
||||
[ -d "$SESSION_DIR" ] && cat "$SESSION_DIR/cmd_status.txt"
|
||||
;;
|
||||
list)
|
||||
[ -d "$SESSIONS_DIR" ] && ls -1 "$SESSIONS_DIR" 2>/dev/null
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
```
|
||||
|
||||
Make executable: `chmod +x .claude/session-exec.sh`
|
||||
|
||||
### 3. Link Sessions Directory (on remote VM)
|
||||
Run on the remote VM to add project sessions to the shared executor:
|
||||
|
||||
```bash
|
||||
# Option A: Symlink project sessions (if using project-local sessions)
|
||||
ln -sfn /path/to/project/.claude/sessions ~/.claude/sessions
|
||||
|
||||
# Option B: Use shared sessions (all projects share one directory)
|
||||
# Just ensure ~/.claude/sessions exists
|
||||
```
|
||||
|
||||
### 4. Create CLAUDE.md
|
||||
Add execution instructions to `.claude/CLAUDE.md`:
|
||||
|
||||
```markdown
|
||||
## Remote Command Execution
|
||||
Uses shared executor at `~/.claude/` on wizard@10.0.10.10.
|
||||
|
||||
### Usage
|
||||
\```bash
|
||||
SESSION_ID=$(.claude/session-exec.sh)
|
||||
echo "command" > .claude/sessions/$SESSION_ID/cmd_input.txt
|
||||
sleep 1 && cat .claude/sessions/$SESSION_ID/cmd_status.txt
|
||||
cat .claude/sessions/$SESSION_ID/cmd_output.txt
|
||||
\```
|
||||
|
||||
Start executor: `~/.claude/remote-executor.sh` (on remote VM)
|
||||
```
|
||||
|
||||
## Shared Executor Location
|
||||
- Scripts: `~/.claude/remote-executor.sh`, `~/.claude/session-exec.sh`
|
||||
- Sessions: `~/.claude/sessions/`
|
||||
- Remote VM: wizard@10.0.10.10
|
||||
97
.claude/skills/terraform-state-identity-mismatch/SKILL.md
Normal file
97
.claude/skills/terraform-state-identity-mismatch/SKILL.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
name: terraform-state-identity-mismatch
|
||||
description: |
|
||||
Fix Terraform "Unexpected Identity Change" errors during plan/apply. Use when:
|
||||
(1) Terraform fails with "the Terraform Provider unexpectedly returned a different
|
||||
identity", (2) State refresh shows identity mismatch between stored and current values,
|
||||
(3) Resource was created but terraform apply timed out, leaving state inconsistent.
|
||||
Solution involves removing and reimporting the affected resource.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2026-01-28
|
||||
---
|
||||
|
||||
# Terraform State Identity Mismatch Fix
|
||||
|
||||
## Problem
|
||||
Terraform fails during plan or apply with an "Unexpected Identity Change" error,
|
||||
indicating the stored state identity doesn't match what the provider returns when
|
||||
reading the resource.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
- Error message contains: "Unexpected Identity Change: During the read operation,
|
||||
the Terraform Provider unexpectedly returned a different identity"
|
||||
- Often occurs after a terraform apply times out mid-creation
|
||||
- Resource exists in the cluster/cloud but state is corrupted
|
||||
- Common with Kubernetes provider after deployment rollout timeouts
|
||||
|
||||
## Solution
|
||||
|
||||
### Step 1: Identify the affected resource
|
||||
The error message includes the resource address:
|
||||
```
|
||||
with module.kubernetes_cluster.module.resume["resume"].kubernetes_deployment.resume
|
||||
```
|
||||
|
||||
### Step 2: Remove from state
|
||||
```bash
|
||||
terraform state rm 'module.kubernetes_cluster.module.resume["resume"].kubernetes_deployment.resume'
|
||||
```
|
||||
Note: Use single quotes around the address to handle brackets properly.
|
||||
|
||||
### Step 3: Import the resource back
|
||||
```bash
|
||||
terraform import 'module.kubernetes_cluster.module.resume["resume"].kubernetes_deployment.resume' <namespace>/<name>
|
||||
```
|
||||
For Kubernetes deployments, the import ID is `namespace/deployment-name`.
|
||||
|
||||
### Step 4: Verify with plan
|
||||
```bash
|
||||
terraform plan -target=<module-path>
|
||||
```
|
||||
Should show minimal or no changes if import was successful.
|
||||
|
||||
### Step 5: Apply to sync any drift
|
||||
```bash
|
||||
terraform apply -target=<module-path>
|
||||
```
|
||||
|
||||
## Verification
|
||||
- `terraform plan` runs without identity errors
|
||||
- `terraform apply` completes successfully
|
||||
- Resource still exists and functions correctly
|
||||
|
||||
## Example
|
||||
**Error:**
|
||||
```
|
||||
Error: Unexpected Identity Change
|
||||
|
||||
Current Identity: cty.ObjectVal(map[string]cty.Value{"api_version":cty.NullVal...})
|
||||
New Identity: cty.ObjectVal(map[string]cty.Value{"api_version":cty.StringVal("apps/v1")...})
|
||||
|
||||
with module.kubernetes_cluster.module.resume["resume"].kubernetes_deployment.resume
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
terraform state rm 'module.kubernetes_cluster.module.resume["resume"].kubernetes_deployment.resume'
|
||||
# Output: Removed ... Successfully removed 1 resource instance(s).
|
||||
|
||||
terraform import 'module.kubernetes_cluster.module.resume["resume"].kubernetes_deployment.resume' resume/resume
|
||||
# Output: Import successful!
|
||||
|
||||
terraform apply -target=module.kubernetes_cluster.module.resume -auto-approve
|
||||
# Output: Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
|
||||
```
|
||||
|
||||
## Notes
|
||||
- This is a provider bug, not user error - consider reporting to provider maintainers
|
||||
- The resource continues to work fine; only the terraform state is affected
|
||||
- Always verify the resource exists before importing (don't import non-existent resources)
|
||||
- For Kubernetes resources, import IDs are typically `namespace/name`
|
||||
- For AWS resources, import IDs vary by resource type (check provider docs)
|
||||
- Consider adding `-lock=false` if state locking causes issues during recovery
|
||||
|
||||
## See Also
|
||||
- Terraform state management documentation
|
||||
- Kubernetes provider import documentation
|
||||
Loading…
Add table
Add a link
Reference in a new issue