Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Apply Customer.io security best practices. Use when implementing secure credential storage, PII handling, webhook signature verification, or GDPR/CCPA compliance. Trigger: "customer.io security", "customer.io pii", "secure customer.io", "customer.io gdpr", "customer.io webhook verify".
.claude/skills/jeremylongshore-customerio-security-basics/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 5% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 92% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 63% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 103% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 64% | 0% |
Implement security best practices for Customer.io: secrets management for API credentials, PII sanitization before sending data, webhook signature verification (HMAC-SHA256), API key rotation, and GDPR/CCPA data deletion compliance.
typescript// lib/customerio-secrets.ts // NEVER hardcode credentials — use environment variables or a secrets manager // Option A: Environment variables (acceptable for most apps) const siteId = process.env.CUSTOMERIO_SITE_ID; const trackKey = process.env.CUSTOMERIO_TRACK_API_KEY; // Option B: GCP Secret Manager (recommended for production) import { SecretManagerServiceClient } from "@google-cloud/secret-manager"; const secretClient = new SecretManagerServiceClient(); async function getSecret(name: string): Promise<string> { const [version] = await secretClient.accessSecretVersion({ name: `projects/my-project/secrets/${name}/versions/latest`, }); return version.payload?.data?.toString() ?? ""; } async function createCioClient() { const [siteId, trackKey] = await Promise.all([ getSecret("customerio-site-id"), getSecret("customerio-track-api-key"), ]); return new TrackClient(siteId, trackKey, { region: RegionUS }); }
typescript// lib/customerio-sanitize.ts // Sanitize user data BEFORE sending to Customer.io const NEVER_SEND = new Set([ "ssn", "social_security", "tax_id", "credit_card", "card_number", "cvv", "password", "password_hash", "bank_account", "routing_number", ]); const HASH_FIELDS = new Set([ "phone", "phone_number", "ip_address", "ip", "address", "street_address", ]); import { createHash } from "crypto"; function hashValue(value: string): string { return createHash("sha256").update(value).digest("hex").substring(0, 16); } export function sanitizeAttributes( attrs: Record<string, any> ): Record<string, any> { const clean: Record<string, any> = {}; for (const [key, value] of Object.entries(attrs)) { const lowerKey = key.toLowerCase(); // Strip highly sensitive fields entirely if (NEVER_SEND.has(lowerKey)) continue; // Hash PII fields if (HASH_FIELDS.has(lowerKey) && typeof value === "string") { clean[`${key}_hash`] = hashValue(value); continue; } clean[key] = value; } return clean; } // Usage import { TrackClient, RegionUS } from "customerio-node"; const cio = new TrackClient(siteId, trackKey, { region: RegionUS }); await cio.identify("user-123", sanitizeAttributes({ email: "user@example.com", // Kept (needed for email delivery) first_name: "Jane", // Kept phone: "+1-555-0123", // Hashed → phone_hash ssn: "123-45-6789", // STRIPPED entirely plan: "pro", // Kept }));
Customer.io signs webhook payloads with HMAC-SHA256. Always verify before processing.
typescript// middleware/customerio-webhook.ts import { createHmac, timingSafeEqual } from "crypto"; import { Request, Response, NextFunction } from "express"; const WEBHOOK_SECRET = process.env.CUSTOMERIO_WEBHOOK_SECRET!; export function verifyCioWebhook( req: Request, res: Response, next: NextFunction ): void { const signature = req.headers["x-cio-signature"] as string; if (!signature) { res.status(401).json({ error: "Missing signature header" }); return; } // req.body must be the raw buffer — configure Express accordingly const rawBody = (req as any).rawBody as Buffer; if (!rawBody) { res.status(500).json({ error: "Raw body not available" }); return; } const expected = createHmac("sha256", WEBHOOK_SECRET) .update(rawBody) .digest("hex"); const valid = timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); if (!valid) { res.status(401).json({ error: "Invalid signature" }); return; } next(); } // Express setup — raw body required for signature verification import express from "express"; const app = express(); app.use("/webhooks/customerio", express.raw({ type: "application/json" })); app.post("/webhooks/customerio", verifyCioWebhook, (req, res) => { const event = JSON.parse((req as any).rawBody.toString()); // Process verified webhook event res.sendStatus(200); });
typescript// scripts/rotate-cio-keys.ts // Rotation procedure — zero downtime async function rotateKeys() { console.log("Customer.io Key Rotation Procedure:"); console.log("1. Go to Settings > Workspace Settings > API & Webhook Credentials"); console.log("2. Click 'Regenerate' next to the key you want to rotate"); console.log("3. Copy the NEW key"); console.log("4. Update your secrets manager:"); console.log(" - GCP: gcloud secrets versions add customerio-track-api-key --data-file=-"); console.log(" - AWS: aws ssm put-parameter --name /cio/track-api-key --value NEW_KEY --overwrite"); console.log("5. Deploy your application (or restart to pick up new secrets)"); console.log("6. Verify: run the connectivity test script"); console.log("7. The old key is immediately invalidated upon regeneration"); console.log(""); console.log("IMPORTANT: Regenerating a key IMMEDIATELY invalidates the old key."); console.log("Update secrets BEFORE regenerating, or plan for brief downtime."); }
typescript// services/customerio-gdpr.ts import { TrackClient, RegionUS } from "customerio-node"; const cio = new TrackClient( process.env.CUSTOMERIO_SITE_ID!, process.env.CUSTOMERIO_TRACK_API_KEY!, { region: RegionUS } ); // GDPR Right to Erasure / CCPA Delete My Data async function handleDeletionRequest(userId: string): Promise<void> { // 1. Suppress — stop all messaging immediately await cio.suppress(userId); console.log(`User ${userId} suppressed (no more messages)`); // 2. Destroy — remove profile and all data from Customer.io await cio.destroy(userId); console.log(`User ${userId} deleted from Customer.io`); // 3. Log the deletion for compliance audit trail console.log(`GDPR deletion completed for ${userId} at ${new Date().toISOString()}`); } // Bulk deletion (e.g., processing deletion requests from a queue) async function bulkDelete(userIds: string[]): Promise<void> { for (const userId of userIds) { try { await handleDeletionRequest(userId); } catch (err: any) { // Log but continue — don't let one failure block others console.error(`Deletion failed for ${userId}: ${err.message}`); } // Respect rate limits await new Promise((r) => setTimeout(r, 100)); } }
.env in production)timingSafeEqual)suppress + destroy).env files in .gitignore| Issue | Solution | |-------|----------| | Credentials exposed in git | Rotate immediately, scan git history with trufflehog | | PII accidentally sent | Delete user with destroy(), update sanitization rules | | Webhook signature mismatch | Verify webhook secret matches Customer.io dashboard | | Key rotation causes downtime | Update secrets manager BEFORE regenerating in dashboard |
After implementing security, proceed to customerio-prod-checklist for production readiness.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | fail→pass | 21,104 | 10,399 | -51% | 1 | 1 | 0% | 4,196 | 4,425 | +5% | 0 | 0 | — |
case-01 | pass→pass | 16,332 | 13,823 | -15% | 1 | 1 | 0% | 3,668 | 5,263 | +43% | 0 | 0 | — |
case-02 | fail→fail | 15,859 | 11,368 | -28% | 1 | 1 | 0% | 3,363 | 4,864 | +45% | 0 | 0 | — |
case-04 | pass→pass | 11,715 | 10,347 | -12% | 1 | 1 | 0% | 2,427 | 4,722 | +95% | 0 | 0 | — |
case-05 | fail→pass | 12,173 | 11,955 | -2% | 1 | 1 | 0% | 2,542 | 4,880 | +92% | 0 | 0 | — |
case-06 | fail→pass | 18,993 | 16,778 | -12% | 1 | 1 | 0% | 3,440 | 5,596 | +63% | 0 | 0 | — |
case-07 | fail→pass | 7,799 | 5,315 | -32% | 1 | 1 | 0% | 1,643 | 3,329 | +103% | 0 | 0 | — |
case-08 | pass→pass | 16,308 | 13,152 | -19% | 1 | 1 | 0% | 3,401 | 5,068 | +49% | 0 | 0 | — |
case-09 | fail→pass | 16,649 | 15,858 | -5% | 1 | 1 | 0% | 3,428 | 5,616 | +64% | 0 | 0 | — |
case-10 | fail→pass | 13,395 | 12,110 | -10% | 1 | 1 | 0% | 2,425 | 4,535 | +87% | 0 | 0 | — |
case-11 | pass→pass | 15,732 | 13,735 | -13% | 1 | 1 | 0% | 2,753 | 4,993 | +81% | 0 | 0 | — |
case-12 | pass→fail | 18,739 | 11,233 | -40% | 1 | 1 | 0% | 3,630 | 4,561 | +26% | 0 | 0 | — |
case-13 | pass→pass | 6,580 | 2,992 | -55% | 1 | 1 | 0% | 1,247 | 2,923 | +134% | 0 | 0 | — |
case-14 | pass→pass | 9,887 | 3,969 | -60% | 1 | 1 | 0% | 1,744 | 2,956 | +69% | 0 | 0 | — |
case-15 | pass→pass | 7,182 | 3,063 | -57% | 1 | 1 | 0% | 1,429 | 2,852 | +100% | 0 | 0 | — |
case-16 | pass→pass | 8,140 | 3,274 | -60% | 1 | 1 | 0% | 1,410 | 2,624 | +86% | 0 | 0 | — |
case-17 | pass→pass | 11,516 | 6,272 | -46% | 1 | 1 | 0% | 2,404 | 3,516 | +46% | 0 | 0 | — |
case-18 | pass→pass | 10,035 | 2,738 | -73% | 1 | 1 | 0% | 1,757 | 2,835 | +61% | 0 | 0 | — |
case-19 | pass→pass | 9,139 | 3,275 | -64% | 1 | 1 | 0% | 1,821 | 2,798 | +54% | 0 | 0 | — |
case-20 | pass→pass | 8,674 | 7,341 | -15% | 1 | 1 | 0% | 1,856 | 3,701 | +99% | 0 | 0 | — |
case-21 | pass→pass | 9,866 | 8,664 | -12% | 1 | 1 | 0% | 1,893 | 3,861 | +104% | 0 | 0 | — |
case-22 | pass→pass | 7,960 | 6,882 | -14% | 1 | 1 | 0% | 1,893 | 3,969 | +110% | 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 +23 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
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.