Community Built

Redact

Replaces supported patterns for contact details, identifiers, cards, and secrets. Review the output before sharing.

By Advait Vaidya

Download action

What it does

Pattern-based text redaction contributed by the community. Handles supported email, URL, IPv4, phone, card, SSN, and labeled-field patterns. Review every result before sharing: some quoted or escaped secrets, phone and address formats, and cards beside other numbers can remain visible. Some ordinary text may be removed. Long inputs can time out. This action does not guarantee anonymization.

How to use it

  1. Download the action file and open it in ActionClip.
  2. Review the action and its destination before installing.
  3. Select the text you want to use and choose Redact.

Requirements

Runs locally in ActionClip. Review the result manually before sharing it.

Privacy

Processes selected text locally without sending it to a service. Sensitive text may remain in the output; review it before sharing.

Example

Input

Name: Example Person
Email: [email protected]
Phone: 202-555-0100

Output

Name: [NAME]
Email: [EMAIL]
Phone: [PHONE]
View action source
function run(selected_text) {
  let text = String(selected_text);

  function redactLabeledValue(labelPattern, replacement) {
    const pattern = new RegExp("(^[ \\t]*(?:" + labelPattern + ")[ \\t]*:[ \\t]*).*$", "gim");
    text = text.replace(pattern, function (_, prefix) {
      return prefix + replacement;
    });
  }

  function redactURLs(value) {
    return value.replace(/\b(?:https?:\/\/|www\.)[^\s<>"']+/gi, function (candidate) {
      let core = candidate;
      let trailing = "";

      while (/[.,;:!?]$/.test(core)) {
        trailing = core.slice(-1) + trailing;
        core = core.slice(0, -1);
      }

      const pairs = [["(", ")"], ["[", "]"], ["{", "}"]];
      for (const pair of pairs) {
        const opener = pair[0];
        const closer = pair[1];
        let balance = 0;
        for (const character of core) {
          if (character === opener) balance += 1;
          if (character === closer) balance -= 1;
        }
        while (balance < 0 && core.endsWith(closer)) {
          core = core.slice(0, -1);
          trailing = closer + trailing;
          balance += 1;
        }
      }

      return "[URL]" + trailing;
    });
  }

  function redactJSONSecrets(value) {
    return value.replace(
      /(["'])(api(?:[ _-]?key)?|access(?:[ _-]?token)?|auth(?:[ _-]?token)?|secret|password|private(?:[ _-]?key)?)\1([ \t]*:[ \t]*)(["'])([^"'\r\n]{8,})\4/gi,
      function (_, keyQuote, label, separator, valueQuote) {
        return keyQuote + label + keyQuote + separator + valueQuote + "[TOKEN]" + valueQuote;
      }
    );
  }

  function redactPlainSecrets(value) {
    const label = "api(?:[ _-]?key)?|access(?:[ _-]?token)?|auth(?:[ _-]?token)?|secret|bearer|password|private(?:[ _-]?key)?";
    const quoted = new RegExp("\\b(" + label + ")([ \\t]*[:=][ \\t]*|[ \\t]+)([\\\"'])([^\\\"'\\r\\n]{8,})\\3", "gi");
    value = value.replace(quoted, function (_, name, separator, quote) {
      return name + separator + quote + "[TOKEN]" + quote;
    });

    const unquoted = new RegExp("\\b(" + label + ")([ \\t]*[:=][ \\t]*|[ \\t]+)([^\\s,;\\\"']{8,})", "gi");
    return value.replace(unquoted, function (_, name, separator) {
      return name + separator + "[TOKEN]";
    });
  }

  function validPaymentCard(candidate) {
    const digits = candidate.replace(/\D/g, "");
    if (digits.length < 13 || digits.length > 19 || /^(\d)\1+$/.test(digits)) return false;

    const groups = candidate.trim().split(/[ -]+/);
    if (groups.length > 1 && groups.every(function (group) { return group.length === 1; })) return false;

    let sum = 0;
    const parity = digits.length % 2;
    for (let index = 0; index < digits.length; index += 1) {
      let digit = Number(digits[index]);
      if (index % 2 === parity) {
        digit *= 2;
        if (digit > 9) digit -= 9;
      }
      sum += digit;
    }
    return sum % 10 === 0;
  }

  function redactPaymentCards(value) {
    return value.replace(/(^|[^\d])((?:\d[ -]?){12,18}\d)(?=$|[^\d])/g, function (match, prefix, candidate) {
      return validPaymentCard(candidate) ? prefix + "[CARD]" : match;
    });
  }

  function validCompactPhone(candidate) {
    const digits = candidate.replace(/\D/g, "");
    return digits.length >= 10 && digits.length <= 15 && !/^(\d)\1+$/.test(digits);
  }

  function redactPhones(value) {
    const extension = "(?:[ \\t]*(?:ext\\.?|extension|x)[ \\t]*\\d{1,6})?";
    const northAmerican = new RegExp(
      "(^|[^A-Za-z0-9])((?:\\+?1[ \\t.-]?)?(?:\\([2-9]\\d{2}\\)|[2-9]\\d{2})[ \\t.-]?[2-9]\\d{2}[ \\t.-]\\d{4}" + extension + ")(?=$|[^A-Za-z0-9])",
      "gim"
    );
    value = value.replace(northAmerican, function (_, prefix) {
      return prefix + "[PHONE]";
    });

    const international = new RegExp(
      "(^|[^A-Za-z0-9])(\\+\\d{1,3}(?:[ \\t-]\\d{2,4}){2,5}" + extension + ")(?=$|[^A-Za-z0-9])",
      "gim"
    );
    value = value.replace(international, function (match, prefix, candidate) {
      return validCompactPhone(candidate) ? prefix + "[PHONE]" : match;
    });

    const compact = new RegExp(
      "(^|[^A-Za-z0-9])(\\+?\\d{10,15}" + extension + ")(?=$|[^A-Za-z0-9])",
      "gim"
    );
    return value.replace(compact, function (match, prefix, candidate) {
      return validCompactPhone(candidate) ? prefix + "[PHONE]" : match;
    });
  }

  function redactAddresses(value) {
    const parts = value.split(/(\r\n|\n|\r)/);
    const label = /^([ \t]*(?:(?:street|mailing|billing|shipping)[ \t]+address|address)[ \t]*:[ \t]*).*$/i;
    const cityRegionPostal = /^[ \t]*[A-Za-z][A-Za-z .'-]{1,60},[ \t]*[A-Z]{2,3}(?:[ \t]+[A-Z0-9][A-Z0-9 -]{2,11})?[ \t]*$/;
    const postalOnly = /^[ \t]*(?:[A-Z]{2}[ \t]+\d{5}(?:-\d{4})?|[A-Z]\d[A-Z][ \t]?\d[A-Z]\d)[ \t]*$/i;

    for (let index = 0; index < parts.length; index += 2) {
      const match = parts[index].match(label);
      if (!match) continue;
      parts[index] = match[1] + "[ADDRESS]";

      let continuationIndex = index + 2;
      let continuationCount = 0;
      while (continuationIndex < parts.length && continuationCount < 2) {
        const line = parts[continuationIndex];
        const isIndented = /^[ \t]+\S/.test(line);
        const isStructuredContinuation = cityRegionPostal.test(line) || postalOnly.test(line);
        if (!isIndented && !isStructuredContinuation) break;
        parts[continuationIndex] = (line.match(/^[ \t]*/) || [""])[0] + "[ADDRESS]";
        continuationIndex += 2;
        continuationCount += 1;
      }
    }
    return parts.join("");
  }

  text = redactURLs(text);
  text = text.replace(/\b[A-Z0-9_%+-](?:[A-Z0-9._%+-]{0,62}[A-Z0-9_%+-])?@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,63}\b/gi, "[EMAIL]");
  text = text.replace(/\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g, "[IPV4]");
  text = text.replace(/\b(?:sk-(?:(?:proj|test|live)[-_])?[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,})\b/g, "[TOKEN]");
  text = redactJSONSecrets(text);
  text = redactPlainSecrets(text);

  text = text.replace(/\b\d{3}[- ]\d{2}[- ]\d{4}\b/g, "[SSN]");
  text = text.replace(/(^[ \t]*(?:ssn|social[ \t]+security(?:[ \t]+number)?)[ \t]*:[ \t]*)\d{9}\b/gim, "$1[SSN]");
  text = redactPaymentCards(text);
  text = redactPhones(text);

  redactLabeledValue("customer(?:\\s+name)?|full\\s+name|contact\\s+name|name", "[NAME]");
  redactLabeledValue("date\\s+of\\s+birth|birth\\s+date|dob", "[DATE]");
  redactLabeledValue("order(?:\\s+id|\\s+number|\\s*#)|invoice(?:\\s+id|\\s+number|\\s*#)", "[ORDER_ID]");
  text = redactAddresses(text);

  return text;
}

Back to Community Built actions