Agent Skills: ClickUp Data Handling

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/clickup-data-handling

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/clickup-pack/skills/clickup-data-handling

Skill Files

Browse the full folder contents for clickup-data-handling.

Download Skill

Loading file tree…

plugins/saas-packs/clickup-pack/skills/clickup-data-handling/SKILL.md

Skill Metadata

Name
clickup-data-handling
Description
|

ClickUp Data Handling

Overview

Handle sensitive data from ClickUp API v2 responses. ClickUp task data often contains PII (assignee emails, names) and business-sensitive information (task descriptions, comments, custom field values).

ClickUp Data Classification

| Data Source | PII Risk | Handling | |-------------|----------|----------| | /user response | High (email, username) | Redact in logs | | /team members | High (emails, names) | Minimize; cache only IDs | | Task assignees | Medium (user IDs, names) | Aggregate when possible | | Task descriptions | Variable (may contain PII) | Scan before storing | | Custom field values | High (email, phone fields) | Encrypt at rest | | Comments | Variable (user content) | Scan before logging | | Webhook payloads | Medium (user objects in history) | Redact before queuing |

PII Detection in ClickUp Data

interface PiiFindings {
  field: string;
  type: string;
  value: string;
}

const PII_PATTERNS = [
  { type: 'email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
  { type: 'phone', regex: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g },
  { type: 'ssn', regex: /\b\d{3}-\d{2}-\d{4}\b/g },
];

function scanClickUpTaskForPii(task: any): PiiFindings[] {
  const findings: PiiFindings[] = [];

  // Check description
  for (const pattern of PII_PATTERNS) {
    const matches = (task.description ?? '').matchAll(pattern.regex);
    for (const m of matches) {
      findings.push({ field: 'description', type: pattern.type, value: m[0] });
    }
  }

  // Check custom fields
  for (const cf of task.custom_fields ?? []) {
    if (cf.type === 'email' && cf.value) {
      findings.push({ field: `custom_field:${cf.name}`, type: 'email', value: cf.value });
    }
    if (cf.type === 'phone' && cf.value) {
      findings.push({ field: `custom_field:${cf.name}`, type: 'phone', value: cf.value });
    }
  }

  // Check assignees
  for (const assignee of task.assignees ?? []) {
    if (assignee.email) {
      findings.push({ field: 'assignee', type: 'email', value: assignee.email });
    }
  }

  return findings;
}

Redaction for Logging

function redactClickUpResponse(data: any): any {
  const redacted = JSON.parse(JSON.stringify(data));

  // Redact user objects
  const redactUser = (user: any) => {
    if (user?.email) user.email = '[REDACTED]';
    if (user?.username) user.username = user.username.substring(0, 2) + '***';
  };

  // Task-level redaction
  if (redacted.assignees) redacted.assignees.forEach(redactUser);
  if (redacted.creator) redactUser(redacted.creator);

  // Webhook payload redaction
  if (redacted.history_items) {
    for (const item of redacted.history_items) {
      if (item.user) redactUser(item.user);
    }
  }

  // Custom fields with PII types
  if (redacted.custom_fields) {
    for (const cf of redacted.custom_fields) {
      if (['email', 'phone'].includes(cf.type) && cf.value) {
        cf.value = '[REDACTED]';
      }
    }
  }

  return redacted;
}

// Use when logging API responses
console.log('[clickup] task fetched:', JSON.stringify(redactClickUpResponse(task)));

Data Export for GDPR/CCPA

async function exportUserClickUpData(userId: number, teamId: string) {
  // 1. Get user profile
  const user = await clickupRequest('/user');

  // 2. Get tasks assigned to user across workspace
  const tasks = await clickupRequest(
    `/team/${teamId}/task?assignees[]=${userId}&include_closed=true`
  );

  // 3. Get time entries by user
  const timeEntries = await clickupRequest(
    `/team/${teamId}/time_entries?assignee=${userId}`
  );

  return {
    exportedAt: new Date().toISOString(),
    source: 'ClickUp API v2',
    userData: {
      id: user.user.id,
      username: user.user.username,
      email: user.user.email,
    },
    tasks: tasks.tasks.map((t: any) => ({
      id: t.id,
      name: t.name,
      status: t.status.status,
      url: t.url,
    })),
    timeEntries: timeEntries.data?.map((e: any) => ({
      id: e.id,
      duration: e.duration,
      description: e.description,
      task_id: e.task?.id,
    })) ?? [],
  };
}

Data Retention

// Track ClickUp API data locally with retention policies
interface RetentionPolicy {
  dataType: string;
  retentionDays: number;
  reason: string;
}

const RETENTION_POLICIES: RetentionPolicy[] = [
  { dataType: 'api_request_logs', retentionDays: 30, reason: 'Debugging' },
  { dataType: 'webhook_events', retentionDays: 90, reason: 'Audit trail' },
  { dataType: 'cached_tasks', retentionDays: 1, reason: 'Performance' },
  { dataType: 'time_entries', retentionDays: 365, reason: 'Billing' },
  { dataType: 'audit_logs', retentionDays: 2555, reason: 'Compliance (7 years)' },
];

async function enforceRetention(db: any) {
  for (const policy of RETENTION_POLICIES) {
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - policy.retentionDays);
    await db.collection(policy.dataType).deleteMany({
      createdAt: { $lt: cutoff },
    });
  }
}

Error Handling

| Issue | Cause | Solution | |-------|-------|----------| | PII in logs | Missing redaction | Wrap all logging with redactClickUpResponse | | GDPR export incomplete | Pagination not handled | Use async generator for full export | | Retention job fails | DB connection | Add retry logic to cron job | | Custom field PII missed | New field types | Re-scan fields via /list/{id}/field |

Resources

Next Steps

For enterprise access control, see clickup-enterprise-rbac.