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 name | Regex (illustrative) | Example match |
|---|---|---|
student-id-number | \b(?:student\s*id|id#|id\s*number)\s*:?\s*\d{4,10}\b | Student ID 9876543, ID# 1234567 |
grade-band | \b(?:K|[1-9]|1[0-2])(?:st|nd|rd|th)?[\s-]+grad(?:e|er)\b | 5th grade, 11th-grader, grade 9 |
school-issued-email | \b[a-z0-9._%+-]+@[a-z0-9.-]*\.(?:k12|edu|sch)\.[a-z.]{2,}\b | jane.doe12@district.k12.ca.us |
labeled-parent-contact | \b(?:parent|guardian)[\s\w]{0,20}(?:phone|contact|number)\s*:?\s*\+?[\d\s().\-]{7,}\b | Parent 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:
- 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. - Custom
Scrubberimplementation. Implement theScrubbercontract directly with your own classifier insidescrub(). The contract is structurally minimal —idfield plus ascrub(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— thecreateStudentSafetyScrubber+createCostCapPolicyimplementations.packages/compliance/src/scrubbers/keyedRegex.ts— the underlying composable scrubber shape.
See also
- Education — the landing page.
- FERPA / COPPA scope — what this scrubber does NOT cover.
- Scrubbers — the redaction contract, per-event-type allowlist, and authoring guide.
@pleach/compliance— the underlying scrubber library andComplianceRuntime.
Supply-chain risk & SBOM
Vendor-neutrality at the SKU boundary, current build artifact + signature posture, and the SBOM / Sigstore / SLSA roadmap.
FERPA / COPPA scope
Honest framing of what @pleach/compliance covers for FERPA and COPPA today, what the educationAgent factory substitutes when you ask for those profiles, and the concrete gap list you should bring to district counsel before signing.