Install any skill in seconds. Free to start, no credit card required.
Get Started Free →California Consumer Privacy Act (CCPA) / CPRA compliance for businesses handling California resident data. Use when serving California users, building privacy features, implementing consumer data rights, or responding to data subject requests.
.claude/skills/terminalskills-ccpa-compliance/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 56% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 97% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 144% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 239% | 0% |
| case-08 | ✓→✓ | = Same ✓ | 171% | 0% |
The California Consumer Privacy Act (CCPA), amended by CPRA (California Privacy Rights Act effective January 1, 2023), gives California residents rights over their personal information. Applies to for-profit businesses that:
Fines: up to $2,500 per unintentional violation, $7,500 per intentional violation. California AG and individual consumers can sue.
| Right | Description | Response Deadline | |-------|-------------|------------------| | Right to Know | What personal info is collected, used, shared, sold | 45 days (+ 45-day extension) | | Right to Delete | Request deletion of personal info | 45 days | | Right to Opt-Out | Opt out of sale or sharing of personal info | Immediate effect | | Right to Correct | Correct inaccurate personal info (CPRA) | 45 days | | Right to Limit | Limit use of sensitive personal info (CPRA) | Immediate effect | | Right to Non-Discrimination | Cannot be denied service for exercising rights | N/A — always | | Right to Data Portability | Receive data in portable format | 45 days |
CPRA adds extra protections for sensitive PI:
Before building DSR workflows, map your data:
python# data_inventory.py — document what personal data you collect DATA_INVENTORY = { "users": { "table": "users", "fields": { "email": {"category": "contact", "sensitive": False, "sold": False}, "name": {"category": "identifier", "sensitive": False, "sold": False}, "ip_address": {"category": "usage", "sensitive": False, "sold": False}, "location": {"category": "location", "sensitive": True, "sold": False}, "phone": {"category": "contact", "sensitive": False, "sold": False}, }, "retention_days": 365 * 3, # 3 years "third_parties": ["Stripe", "SendGrid", "Mixpanel"], }, "analytics_events": { "table": "events", "fields": { "user_id": {"category": "identifier", "sensitive": False, "sold": False}, "event_name": {"category": "behavior", "sensitive": False, "sold": False}, "device_id": {"category": "identifier", "sensitive": False, "sold": True}, }, "retention_days": 365, "third_parties": ["Mixpanel", "Segment"], } }
Your privacy policy must disclose:
javascript// Required sections in privacy policy const REQUIRED_DISCLOSURES = { collected_categories: [ "Identifiers (name, email, IP address)", "Commercial information (purchase history)", "Internet or other network activity (browsing history)", "Geolocation data", "Inferences drawn from above" ], collection_purposes: [ "Provide and improve our services", "Send transactional and marketing emails", "Analytics and product development" ], sells_data: false, // Required disclosure shares_data: true, // Sharing = cross-context behavioral advertising shared_with: ["Google Analytics", "Facebook Pixel", "Mixpanel"], rights_contact: "privacy@yourcompany.com", opt_out_url: "https://yourcompany.com/privacy/opt-out" };
GPC is a browser signal that automatically invokes the right to opt-out of sale/sharing. California law (CPRA) requires businesses to honor it as of 2023.
javascript// Express.js middleware — detect GPC signal and honor opt-out const gpcMiddleware = (req, res, next) => { const gpcEnabled = req.headers['sec-gpc'] === '1'; if (gpcEnabled) { // Auto-apply opt-out for this request req.privacyConsent = { optedOutOfSale: true, optedOutOfSharing: true, source: 'gpc_signal', detectedAt: new Date().toISOString() }; // Record opt-out preference if (req.user) { recordOptOut(req.user.id, 'gpc_signal'); } else { // Use cookie to persist for anonymous users res.cookie('ccpa_optout', '1', { maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year httpOnly: true, secure: true, sameSite: 'Strict' }); } } next(); };
python# FastAPI DSR endpoints from fastapi import FastAPI, BackgroundTasks, HTTPException from pydantic import BaseModel, EmailStr from enum import Enum import uuid from datetime import datetime app = FastAPI() class DSRType(str, Enum): KNOW = "know" DELETE = "delete" CORRECT = "correct" OPT_OUT = "opt_out" LIMIT_SPI = "limit_sensitive" PORTABILITY = "portability" class DSRRequest(BaseModel): request_type: DSRType email: EmailStr name: str correction_details: str = None # For CORRECT requests class DSRResponse(BaseModel): request_id: str status: str deadline: str message: str @app.post("/api/privacy/dsr", response_model=DSRResponse) async def submit_dsr(request: DSRRequest, background_tasks: BackgroundTasks): """Submit a Data Subject Request.""" request_id = str(uuid.uuid4()) deadline_days = 1 if request.request_type == DSRType.OPT_OUT else 45 # Store request dsr_record = { "id": request_id, "type": request.request_type, "email": request.email, "name": request.name, "status": "pending", "submitted_at": datetime.utcnow().isoformat(), "deadline_days": deadline_days, "verified": False } await db.dsr_requests.insert(dsr_record) # Send verification email background_tasks.add_task(send_verification_email, request.email, request_id) return DSRResponse( request_id=request_id, status="pending_verification", deadline=f"{deadline_days} days after identity verification", message="We've sent a verification email. Please verify your identity to proceed." ) @app.post("/api/privacy/dsr/{request_id}/verify") async def verify_dsr(request_id: str, token: str, background_tasks: BackgroundTasks): """Verify identity and begin DSR processing.""" dsr = await db.dsr_requests.find_one({"id": request_id, "token": token}) if not dsr: raise HTTPException(status_code=404, detail="Request not found") await db.dsr_requests.update({"id": request_id}, {"verified": True, "verified_at": datetime.utcnow().isoformat()}) background_tasks.add_task(process_dsr, request_id, dsr["type"], dsr["email"]) return {"status": "processing", "message": "Identity verified. Processing your request."} async def process_dsr(request_id: str, dsr_type: DSRType, email: str): """Process DSR by type.""" user = await db.users.find_one({"email": email}) if not user: await complete_dsr(request_id, "no_data_found") return if dsr_type == DSRType.DELETE: await delete_user_data(user["id"]) elif dsr_type == DSRType.KNOW: data_export = await export_user_data(user["id"]) await send_data_export(email, data_export) elif dsr_type == DSRType.OPT_OUT: await opt_out_user(user["id"]) elif dsr_type == DSRType.PORTABILITY: portable_data = await export_portable_data(user["id"]) await send_data_export(email, portable_data, format="json") await complete_dsr(request_id, "completed")
pythonasync def export_user_data(user_id: str) -> dict: """Export all personal data for a user — CCPA Right to Know.""" user = await db.users.find_one({"id": user_id}) orders = await db.orders.find({"user_id": user_id}) events = await db.analytics_events.find({"user_id": user_id}) return { "export_date": datetime.utcnow().isoformat(), "profile": { "name": user["name"], "email": user["email"], "phone": user.get("phone"), "created_at": user["created_at"] }, "purchase_history": [ {"order_id": o["id"], "date": o["date"], "amount": o["amount"]} for o in orders ], "analytics_events": [ {"event": e["name"], "date": e["timestamp"]} for e in events ], "third_party_sharing": [ {"vendor": "Stripe", "data": "Payment processing"}, {"vendor": "SendGrid", "data": "Email delivery"}, ] }
javascript// Track and honor opt-out preference async function recordOptOut(userId, source) { await db.privacyPreferences.upsert({ userId, optedOutOfSale: true, optedOutOfSharing: true, source, // 'user_request' | 'gpc_signal' | 'cookie_banner' timestamp: new Date().toISOString() }); // Propagate opt-out to third parties await Promise.all([ mixpanel.optOut(userId), segment.suppress(userId), // Don't forget to stop sharing with ad networks ]); }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 13,004 | 10,909 | -16% | 1 | 1 | 0% | 2,699 | 5,325 | +97% | 0 | 0 | — |
case-02 | fail→pass | 15,042 | 14,740 | -2% | 1 | 1 | 0% | 3,678 | 5,726 | +56% | 0 | 0 | — |
case-03 | fail→fail | 14,231 | 8,160 | -43% | 1 | 1 | 0% | 3,144 | 4,975 | +58% | 0 | 0 | — |
case-04 | pass→pass | 8,630 | 6,167 | -29% | 1 | 1 | 0% | 1,816 | 4,422 | +144% | 0 | 0 | — |
case-05 | pass→pass | 5,617 | 3,269 | -42% | 1 | 1 | 0% | 1,044 | 3,542 | +239% | 0 | 0 | — |
case-06 | fail→fail | 5,218 | 6,796 | +30% | 1 | 1 | 0% | 879 | 4,181 | +376% | 0 | 0 | — |
case-07 | fail→fail | 3,423 | 4,262 | +25% | 1 | 1 | 0% | 637 | 3,727 | +485% | 0 | 0 | — |
case-08 | pass→pass | 7,516 | 3,833 | -49% | 1 | 1 | 0% | 1,328 | 3,596 | +171% | 0 | 0 | — |
case-09 | pass→pass | 5,810 | 5,558 | -4% | 1 | 1 | 0% | 1,033 | 3,976 | +285% | 0 | 0 | — |
case-10 | pass→pass | 12,121 | 10,468 | -14% | 1 | 1 | 0% | 2,169 | 4,904 | +126% | 0 | 0 | — |
case-11 | pass→pass | 10,476 | 9,188 | -12% | 1 | 1 | 0% | 1,915 | 4,513 | +136% | 0 | 0 | — |
case-12 | pass→pass | 12,140 | 15,717 | +29% | 1 | 1 | 0% | 2,105 | 4,859 | +131% | 0 | 0 | — |
case-13 | pass→pass | 10,772 | 16,284 | +51% | 1 | 1 | 0% | 1,935 | 5,956 | +208% | 0 | 0 | — |
case-14 | pass→pass | 10,386 | 11,811 | +14% | 1 | 1 | 0% | 1,870 | 4,615 | +147% | 0 | 0 | — |
case-15 | pass→pass | 5,942 | 5,815 | -2% | 1 | 1 | 0% | 1,042 | 3,818 | +266% | 0 | 0 | — |
case-16 | pass→pass | 5,669 | 5,394 | -5% | 1 | 1 | 0% | 1,089 | 3,877 | +256% | 0 | 0 | — |
case-17 | pass→pass | 6,088 | 4,798 | -21% | 1 | 1 | 0% | 1,079 | 3,694 | +242% | 0 | 0 | — |
case-18 | pass→pass | 5,214 | 7,786 | +49% | 1 | 1 | 0% | 1,081 | 4,287 | +297% | 0 | 0 | — |
case-19 | pass→pass | 4,103 | 3,637 | -11% | 1 | 1 | 0% | 819 | 3,592 | +339% | 0 | 0 | — |
case-20 | pass→pass | 5,866 | 5,662 | -3% | 1 | 1 | 0% | 1,105 | 3,939 | +256% | 0 | 0 | — |
case-21 | pass→pass | 8,148 | 8,844 | +9% | 1 | 1 | 0% | 1,279 | 4,298 | +236% | 0 | 0 | — |
case-22 | pass→pass | 7,987 | 7,251 | -9% | 1 | 1 | 0% | 1,614 | 4,059 | +151% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.