pleach
Use cases / Education

Student safety

The shipped student-safety scrubber from @pleach/compliance/education — what it redacts today, the composable KeyedRegexScrubber shape for district-specific extensions, and the honest gap on per-category content moderation.

createStudentSafetyScrubber is the conservative default for redacting student-identifying fragments before they cross the LLM boundary. It composes KeyedRegexScrubber — same shape as the shipped SSN / credit-card / driver's-license scrubbers — so the result drops into any ComplianceRuntime scrubber chain without new types.

The scrubber is wired automatically when you use educationAgent. This page is for when you need to extend it, tune it, or wire it into a different runtime (e.g. a teacher-facing transcription pipeline that runs outside @pleach/recipes).

What it redacts today

Four pattern categories ship as defaults. The match is tuned to favor false positives over false negatives — a missed student-ID leakage is a worse failure than over-redaction in a classroom transcript.

Pattern nameRegex (illustrative)Example match
student-id-number\b(?:student\s*id|id#|id\s*number)\s*:?\s*\d{4,10}\bStudent ID 9876543, ID# 1234567
grade-band\b(?:K|[1-9]|1[0-2])(?:st|nd|rd|th)?[\s-]+grad(?:e|er)\b5th grade, 11th-grader, grade 9
school-issued-email\b[a-z0-9._%+-]+@[a-z0-9.-]*\.(?:k12|edu|sch)\.[a-z.]{2,}\bjane.doe12@district.k12.ca.us
labeled-parent-contact\b(?:parent|guardian)[\s\w]{0,20}(?:phone|contact|number)\s*:?\s*\+?[\d\s().\-]{7,}\bParent phone: (555) 123-4567

Default replacement is [STUDENT]. Override per scrubber, or per-entry on extra patterns.

A district that wants to NARROW the default match — for example, to disable grade-band redaction in a research-only context where grade is part of the analysis dimension — should compose its own KeyedRegexScrubber directly rather than try to subtract from the default set.

Minimal usage

import { createStudentSafetyScrubber } from "@pleach/compliance/education";

const scrubber = createStudentSafetyScrubber();

const result = scrubber.scrub(
  "Hi — Student ID 9876543 needs help with 5th grade fractions",
  {
    eventType: "user.message",
    fieldPath: ["content"],
    tenantId: "district-1234",
  },
);

// result.redacted   : "Hi — [STUDENT] needs help with [STUDENT] fractions"
// result.matchCount : 2
// result.matchedScrubberIds : ["student-safety:student-id-number",
//                              "student-safety:grade-band"]

The scrubber result shape mirrors every other Scrubber in @pleach/compliance{ redacted, matchCount, matchedScrubberIds }. Audit ledgers can read matchedScrubberIds to attribute which pattern fired, without needing to re-run the scrubber.

Extending with district-specific patterns

Districts and platforms accumulate their own identifier shapes: internal student-information-system IDs, locker numbers, bus-route codes, IEP-document filename prefixes. Pass them through extra:

import { createStudentSafetyScrubber } from "@pleach/compliance/education";

const scrubber = createStudentSafetyScrubber({
  id: "district-1234-student-safety",
  replacement: "[STUDENT]",
  extra: [
    // District-issued 8-digit student ID with leading `D`.
    { name: "district-id", pattern: /\bD\d{8}\b/g, replacement: "[DID]" },
    // Bus route on a transportation transcript.
    { name: "bus-route", pattern: /\bRoute\s+\d{1,3}[A-Z]?\b/gi },
    // IEP filename pattern. Replacement defaults to the scrubber's.
    { name: "iep-doc", pattern: /\bIEP-\d{6}\.pdf\b/g },
  ],
});

extra runs AFTER the defaults in insertion order. Use a per-entry replacement to override the scrubber-level default (useful when downstream code distinguishes redaction markers by shape — [STUDENT] vs [DID]).

Composing with KeyedRegexScrubber directly

createStudentSafetyScrubber() is a thin factory over KeyedRegexScrubber. When you need full control over pattern ordering, attribution, or want to stack multiple keyed-regex scrubbers in a deterministic order, construct KeyedRegexScrubber directly:

import { KeyedRegexScrubber } from "@pleach/compliance/scrubbers";

const districtScrubber = new KeyedRegexScrubber({
  id: "district-1234-overrides",
  entries: [
    { name: "internal-ticket", pattern: /TKT-\d{6}/g, replacement: "[TKT]" },
    { name: "lunch-account", pattern: /LUNCH\d{8}/g, replacement: "[LUNCH]" },
  ],
});

// Chain in your ComplianceRuntime:
// scrubbers: [createStudentSafetyScrubber(), districtScrubber, ...]

Per the KeyedRegexScrubber contract, the consumer owns the false-positive rate on its patterns. Idempotency is a consumer responsibility — chained pathological patterns (e.g. { pattern: /\d+/, replacement: "0" }) are a config error, not a scrubber bug.

Wiring into a custom ComplianceRuntime

When you're not using educationAgent — for example, a teacher-facing transcript-correction pipeline that runs separately from the chatbot — compose the scrubber into a ComplianceRuntime directly:

import {
  createComplianceRuntime,
  SsnUsScrubber,
} from "@pleach/compliance";
import { createStudentSafetyScrubber } from "@pleach/compliance/education";

const runtime = createComplianceRuntime({
  scrubbers: [
    createStudentSafetyScrubber(),
    new SsnUsScrubber(), // belt-and-suspenders for parent SSNs on emergency forms
  ],
});

// In your per-event boundary:
const { redacted, matchCount } = runtime.scrub(transcript, {
  eventType: "transcript.captured",
  fieldPath: ["content"],
  tenantId: districtId,
});

Honest gap — content moderation

The shipped scrubber redacts identifiers. Per-category content safety (e.g. "self-harm", "violence", "age-inappropriate language") is a separate concern that is NOT covered today.

educationAgent accepts a contentSafety array but the values are surfaced on the runtime tag for telemetry only. The shipped scrubber does NOT have per-category branches. Two paths if you need category moderation today:

  1. External moderation API. Wire the Anthropic moderation endpoint, the OpenAI moderation endpoint, or a vendor like Hive / Perspective in front of ask(). Reject or escalate based on the verdict before the message reaches the model.
  2. Custom Scrubber implementation. Implement the Scrubber contract directly with your own classifier inside scrub(). The contract is structurally minimal — id field plus a scrub(input, ctx) method — so a classifier-backed scrubber drops into the same chain.

The category-level enforcement gap is tracked on the roadmap.

Source

  • packages/compliance/src/education/studentSafety.ts — the createStudentSafetyScrubber + createCostCapPolicy implementations.
  • packages/compliance/src/scrubbers/keyedRegex.ts — the underlying composable scrubber shape.

See also

On this page