Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Keeping personal data out of prompts

After this lesson: I can replace personal data with placeholders before a model call and restore them afterwards.

65 min lesson7 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

The ticket with a phone number in it

Keeping personal data out of prompts means a model only sees what it needs to do the task. It matters because a prompt is sent to another service, and it often ends up in a log.

A support tool sends each ticket to a model to draft a reply. The ticket holds a name, an email address and a phone number. The reply needs none of them. The tool can swap them for placeholders and swap them back.

By the end of this lesson

Note

I can replace personal data with placeholders before a model call and restore them afterwards.

Key words

WordWhat it meansExample
Personal dataany detail that identifies a living person
Data minimisationsending only what the task needs
Redactto replace a value with a stand-in
Placeholdera label that stands for a removed value[EMAIL_1]
Mappingthe table that links each placeholder to its value

The cells students fill in are left blank in this preview.

Send only what the task needs

Ask first whether the model needs the value at all. A reply to a complaint needs the complaint, not the phone number. Remove what is not needed.

If the reply must mention a person, use a placeholder. The model writes "Dear [NAME_1]" and your own code puts the real name back afterwards.

Worked example: before and after

ValueSent to the modelKept in the mapping
Amira[NAME_1][NAME_1] is Amira
[email protected][EMAIL_1][EMAIL_1] is [email protected]
07700900123[PHONE_1][PHONE_1] is 07700900123
Amira, again[NAME_1]The same value gets the same placeholder

The cells students fill in are left blank in this preview.

Redaction has limits

A rule finds the shapes you wrote: a word with an at sign, a long run of digits, a name from a list. It misses a street name, a nickname or a number written with spaces.

So redaction lowers the risk and never removes it. Test it on messy examples, and keep a check that looks for leftovers.

Write the model helper

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    return (await res.json()).choices[0].message.content;' '  } catch (error) {' '    console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' '    const list = [].concat(sample);' '    const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' '    return list[Math.min(turn, list.length - 1)];' '  }' '}' 'module.exports = { ask };' > model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the redaction script, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'const names = [\"Amira\", \"Tom\"];' 'const ticket = \"Hi, I am Amira. Email [email protected] or call 07700900123. Tom helped me last week.\";' 'function allDigits(text) {' '  return text.length >= 8 && text.split(\"\").every(function (c) { return c >= \"0\" && c <= \"9\"; });' '}' > redact.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the redaction script, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function redact(text, names) {' '  const map = {};' '  const seen = {};' '  const words = text.split(\" \");' '  for (let i = 0; i < words.length; i++) {' '    let bare = words[i];' '    while (bare.endsWith(\".\") || bare.endsWith(\",\")) bare = bare.slice(0, -1);' '    const tail = words[i].slice(bare.length);' '    let kind = \"\";' '    if (bare.includes(\"@\")) kind = \"EMAIL\";' '    if (allDigits(bare)) kind = \"PHONE\";' '    if (names.includes(bare)) kind = \"NAME\";' >> redact.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the redaction script, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' '    if (kind === \"\") continue;' '    if (seen[bare] === undefined) {' '      const n = Object.keys(map).filter(function (k) { return k.startsWith(\"[\" + kind); }).length + 1;' '      seen[bare] = \"[\" + kind + \"_\" + n + \"]\";' '      map[seen[bare]] = bare;' '    }' '    words[i] = seen[bare] + tail;' '  }' '  return { text: words.join(\" \"), map: map };' '}' >> redact.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the redaction script, part 4

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function restore(text, map) {' '  let out = text;' '  for (const key of Object.keys(map)) out = out.split(key).join(map[key]);' '  return out;' '}' 'async function main() {' '  const safe = redact(ticket, names);' '  console.log(\"Sent to the model: \" + safe.text);' '  console.log(\"An @ is left in it: \" + safe.text.includes(\"@\"));' '  const rule = \"Write a two-sentence reply to the customer. Keep every placeholder in square brackets exactly as written.\";' '  const messages = [{ role: \"system\", content: rule }, { role: \"user\", content: safe.text }];' '  const sample = \"Dear [NAME_1], thank you for your message. We will write to [EMAIL_1] today.\";' >> redact.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the redaction script, part 5

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  const reply = await ask(messages, sample, 100);' '  console.log(\"Reply with placeholders: \" + reply);' '  console.log(\"Restored for the customer: \" + restore(reply, safe.map));' '  console.log(\"Log line: redacted \" + Object.keys(safe.map).length + \" items, reply length \" + reply.length);' '}' 'main();' >> redact.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run the redaction script

Command
docker exec -w /home/node/project agent node redact.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

One possible output

LineWhat it shows
Sent to the modelThe ticket with [NAME_1], [EMAIL_1], [PHONE_1] and [NAME_2] in place of the values
An @ is left in itfalse
Reply with placeholdersTwo sentences that keep the placeholders. The words differ each run
Restored for the customerThe reply with the real values put back
Log lineCounts and a length, with no personal text

The cells students fill in are left blank in this preview.

What a log may hold

A log is read by more people than the app, and it lives longer. Log what you need to find a fault: a request id, a user id, a count, a time, a decision.

Do not log the prompt or the reply in full. If you must keep one to debug, keep the redacted copy, for a short time, behind access control.

A common mistake

Warning

Many people think that once the prompt is redacted, the logs are safe. The reply, the error messages and the original request may still hold the real values. Check every place the text goes: the prompt, the reply, the logs and any cache.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.