Course 4 covered keeping sensitive fields out of an ability’s response to an AI agent. This lesson covers the other direction of the same problem: keeping personal data out of the prompts your own WordPress code constructs when it calls an AI model directly, using the PHP AI Client SDK, for something like summarization, moderation, or classification. Both are the same underlying discipline, decide what an AI system actually needs to see, and never hand it more, applied at two different points in the pipeline: what an ability returns, and what a prompt contains.
Course 4’s “Protecting Sensitive Data from AI Agents” lesson, and Course 5’s PHP AI Client SDK overview. This lesson assumes you’re already constructing prompts from WordPress data somewhere in your codebase.
Step 1: Two different places PII can reach a model, and why both matter
An ability’s output_schema governs what comes back from a WordPress action an agent
calls. A prompt your own plugin code builds, to summarize a support ticket, moderate a
comment, or classify a product review, governs what goes into a model in the other
direction. Both are data-minimization problems, and treating only one of them carefully
leaves a real gap.
| Direction | Where it’s controlled | Covered in |
|---|---|---|
| WordPress action to AI agent | output_schema and a deliberately narrow execute_callback return | Course 4, Lesson 6 |
| WordPress code to AI model (via PHP AI Client SDK) | What your code includes when it constructs a prompt string | This lesson |
Step 2: Redact before you build the prompt, not after
The discipline from Course 4 applies here too: don’t pass through a full database row, comment object, or order object into a prompt template and trust that the model won’t echo something sensitive back. Build the text that reaches the model deliberately, and redact known-sensitive patterns before that text is ever concatenated into the request.
// File: my-plugin.php
function my_plugin_redact_pii( string $text ): string {
// Emails.
$text = preg_replace( '/[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/', '[redacted-email]', $text );
// Phone numbers, a permissive pattern covering common formats.
$text = preg_replace( '/\+?\d[\d\s().-]{7,}\d/', '[redacted-phone]', $text );
return $text;
}
Apply it immediately before constructing the request, not as a step you might remember to add later:
// File: my-plugin.php
function my_plugin_summarize_ticket( int $ticket_id ) {
$ticket = my_plugin_get_ticket( $ticket_id );
// Redact before this ever becomes part of a prompt string.
$safe_body = my_plugin_redact_pii( $ticket->body );
$prompt = "Summarize this support ticket in two sentences:\n\n" . $safe_body;
return ai_client()
->using_provider( 'anthropic' )
->generate_text( $prompt );
}
It’s an easy mistake to redact a value for logging purposes and then separately, further down the function, build the prompt from the original unredacted variable. Redact once, early, and use only the redacted variable for everything downstream, prompt construction and logging alike.
Step 3: Combine with narrow selection, not just redaction
Regex redaction catches known patterns, emails, phone numbers, and with more effort, some name and address formats. It won’t catch everything, and it shouldn’t be the only line of defense. Pair it with the same practice from Course 4: only pull the specific fields a prompt actually needs, rather than handing an entire object to a templating function and redacting the result.
// File: my-plugin.php
// Weaker: passes the whole object through, redaction has to catch everything.
$prompt = "Summarize:\n\n" . my_plugin_redact_pii( print_r( $ticket, true ) );
// Stronger: select only the fields the summary actually needs, then redact those.
$prompt = "Summarize this support ticket:\n\n" . my_plugin_redact_pii( $ticket->subject . "\n" . $ticket->body );
Step 4: Know the limits of regex-based redaction
Regex patterns reliably catch structured data like emails and standard phone number formats. They’re much less reliable against names, physical addresses, or personal details embedded in free-form prose, “my daughter Maya’s school” isn’t something a regex pattern will reasonably catch. Where a feature routinely processes free text likely to contain this kind of information, more robust approaches exist, a named-entity recognition pass, an allowlist of only specific structured fields rather than free text at all, or simply not sending that category of content to an external model in the first place. Treat regex redaction as a real, useful layer, not a complete solution.
A redaction function that runs successfully isn’t proof that no personal data reached the prompt, only that the specific patterns it was written to catch didn’t appear in that specific input. Periodically review real (redacted) request logs by hand to check for personal data your patterns are missing.
Test it: confirm redaction actually runs
wp-env run cli wp eval '
$text = "Contact Jane at jane@example.com or 555-123-4567 about her order.";
echo my_plugin_redact_pii( $text );
'
You should see the email and phone number replaced, and the rest of the sentence intact. Then trace one real prompt-construction function in your codebase and confirm redaction runs on the exact string that gets sent, not a separate copy.
Recap
Course 4 handled what an ability hands back to an agent. This lesson handles the other direction: what your own code sends to a model. Select only the fields a prompt actually needs, redact known PII patterns from that selected text immediately before construction, and treat redaction as one useful layer rather than a guarantee, since free-form prose can carry personal data no regex reliably catches.
Resources & further reading
- Abilities API reference, developer.wordpress.org
- Privacy guide for plugin developers, developer.wordpress.org
- WordPress MCP Security & Authentication, “Protecting Sensitive Data from AI Agents”, Course 4