[{"data":1,"prerenderedAt":1399},["ShallowReactive",2],{"blog-pii-redaction-adk":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"image":11,"tags":12,"body":18,"_type":1393,"_id":1394,"_source":1395,"_file":1396,"_stem":1397,"_extension":1398},"/blog/pii-redaction-adk","blog",false,"","How to Keep Sensitive Data Out of Your LLM","Input Redaction for Google ADK Agents","2026-06-24","/images/pii-redaction-banner.svg",[13,14,15,16,17],"ai","llm","python","security","adk",{"type":19,"children":20,"toc":1382},"root",[21,29,34,50,82,87,94,171,176,181,186,191,196,210,241,246,251,328,333,660,689,694,723,887,892,962,967,992,997,1010,1030,1042,1128,1133,1138,1163,1168,1173,1183,1193,1203,1213,1223,1228,1233,1278,1283,1288,1293,1376],{"type":22,"tag":23,"props":24,"children":25},"element","p",{},[26],{"type":27,"value":28},"text","Most sensitive data doesn't leak out of an LLM app through a clever attack. It leaks because your logging stack did exactly what it was built to do: it logged the request. Imagine: your user pastes a support ticket into a prompt, and now a name, email, and account number inside it land in a trace nobody meant to keep. You weren't hacked. The data is just going where you told it to go.",{"type":22,"tag":23,"props":30,"children":31},{},[32],{"type":27,"value":33},"That's a risk many teams ignore until after they ship. One prompt fans out. The text goes to your LLM provider, where it sits in request logs and a prompt cache. Your own instrumentation forwards it again: an APM like Datadog, LLM tracing tools like LangSmith or Langfuse, log aggregators like Splunk or CloudWatch, an error tracker like Sentry, maybe a third-party eval pipeline that replays real traffic, maybe your internal data lake. You weren't trying to leak anything, yet that same PII now sits in multiple systems you don't fully control and can't easily purge.",{"type":22,"tag":23,"props":35,"children":36},{},[37,39,48],{"type":27,"value":38},"LLM security fixates on what the model can be tricked into saying: prompt injection, jailbreaks, the mistakes that go viral. But the inputs are equally important. Ship to enterprise customers and the PII moving through your prompts isn't only yours to lose. This isn't niche: in the 2025 ",{"type":22,"tag":40,"props":41,"children":45},"a",{"href":42,"rel":43},"https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/",[44],"nofollow",[46],{"type":27,"value":47},"OWASP Top 10 for LLM Applications",{"type":27,"value":49},", Sensitive Information Disclosure climbed to LLM02, up from number six the year before. This is what happens when LLM apps hit production.",{"type":22,"tag":23,"props":51,"children":52},{},[53,55,62,64,71,73,80],{"type":27,"value":54},"It's also law, not just risk. GDPR's data-minimization principle (",{"type":22,"tag":40,"props":56,"children":59},{"href":57,"rel":58},"https://gdpr-info.eu/art-5-gdpr/",[44],[60],{"type":27,"value":61},"Article 5",{"type":27,"value":63},") is the legal name for the engineering rule here: don't process personal data you don't need. ",{"type":22,"tag":40,"props":65,"children":68},{"href":66,"rel":67},"https://gdpr-info.eu/art-17-gdpr/",[44],[69],{"type":27,"value":70},"GDPR Article 17",{"type":27,"value":72}," and California's ",{"type":22,"tag":40,"props":74,"children":77},{"href":75,"rel":76},"https://oag.ca.gov/privacy/ccpa",[44],[78],{"type":27,"value":79},"CCPA",{"type":27,"value":81}," both let a person demand their data be deleted, and you can't delete what you can't find. Once a customer's name has fanned out into months of Datadog logs, a vendor's eval set, and a provider's prompt cache, \"delete everything you hold on me\" is a promise you can't keep. Add cross-border transfer rules and data-processing agreements that never named your observability stack as a PII destination, and suddenly your compliance strategy starts to fall apart.",{"type":22,"tag":23,"props":83,"children":84},{},[85],{"type":27,"value":86},"There is a fix that works at every one of those layers: redact PII before the model sees it, so it never enters the context window in the first place. Nothing sensitive gets logged, so there's nothing to find, transfer, or fail to delete. The rest of this post shows how, for agents built on Google's Agent Development Kit (ADK), using the open-source Microsoft Presidio library.",{"type":22,"tag":88,"props":89,"children":91},"h2",{"id":90},"contents",[92],{"type":27,"value":93},"Contents",{"type":22,"tag":95,"props":96,"children":97},"ul",{},[98,108,117,126,135,144,153,162],{"type":22,"tag":99,"props":100,"children":101},"li",{},[102],{"type":22,"tag":40,"props":103,"children":105},{"href":104},"#why-agents-make-this-worse",[106],{"type":27,"value":107},"Why agents make this worse",{"type":22,"tag":99,"props":109,"children":110},{},[111],{"type":22,"tag":40,"props":112,"children":114},{"href":113},"#the-approach-redact-before-the-model-sees-it",[115],{"type":27,"value":116},"The approach: redact before the model sees it",{"type":22,"tag":99,"props":118,"children":119},{},[120],{"type":22,"tag":40,"props":121,"children":123},{"href":122},"#setting-up-presidio",[124],{"type":27,"value":125},"Setting up Presidio",{"type":22,"tag":99,"props":127,"children":128},{},[129],{"type":22,"tag":40,"props":130,"children":132},{"href":131},"#the-before-model-callback",[133],{"type":27,"value":134},"The before-model callback",{"type":22,"tag":99,"props":136,"children":137},{},[138],{"type":22,"tag":40,"props":139,"children":141},{"href":140},"#but-what-if-a-tool-needs-pii",[142],{"type":27,"value":143},"But what if a tool needs PII?",{"type":22,"tag":99,"props":145,"children":146},{},[147],{"type":22,"tag":40,"props":148,"children":150},{"href":149},"#tradeoffs",[151],{"type":27,"value":152},"Tradeoffs",{"type":22,"tag":99,"props":154,"children":155},{},[156],{"type":22,"tag":40,"props":157,"children":159},{"href":158},"#conclusion",[160],{"type":27,"value":161},"Conclusion",{"type":22,"tag":99,"props":163,"children":164},{},[165],{"type":22,"tag":40,"props":166,"children":168},{"href":167},"#further-reading",[169],{"type":27,"value":170},"Further reading",{"type":22,"tag":88,"props":172,"children":174},{"id":173},"why-agents-make-this-worse",[175],{"type":27,"value":107},{"type":22,"tag":23,"props":177,"children":178},{},[179],{"type":27,"value":180},"A single LLM call is a small attack surface: one request, one response, one place to inspect. Agents compound the problem. The agent keeps memory across turns, and the leaks multiply. It makes tool calls, often with raw user input. It hands off to other agents. Maybe it's wired to a trace exporter for debugging. Every one of those is a place you can leak sensitive data.",{"type":22,"tag":23,"props":182,"children":183},{},[184],{"type":27,"value":185},"So an initial check isn't enough. Sanitize the first message, then watch the agent write raw PII into session state or pass it to a tool that logs its arguments, and you've leaked anyway. The control has to fire every time the agent talks to the model, not just once.",{"type":22,"tag":88,"props":187,"children":189},{"id":188},"the-approach-redact-before-the-model-sees-it",[190],{"type":27,"value":116},{"type":22,"tag":23,"props":192,"children":193},{},[194],{"type":27,"value":195},"The law is: don't process PII you don't need. Keep PII out of the context window and there's nothing to leak downstream. You can't leak what was never there.",{"type":22,"tag":23,"props":197,"children":198},{},[199,201,208],{"type":27,"value":200},"ADK gives you a clean place to enforce this: the ",{"type":22,"tag":40,"props":202,"children":205},{"href":203,"rel":204},"https://adk.dev/callbacks/types-of-callbacks/",[44],[206],{"type":27,"value":207},"before-model callback",{"type":27,"value":209},". It fires after ADK assembles the request but before that request goes to the model. That's the seam a redaction layer wants: late enough to see the final assembled input, early enough that the model never receives the raw version.",{"type":22,"tag":23,"props":211,"children":212},{},[213,215,222,224,231,233,239],{"type":27,"value":214},"For detection and replacement you can use ",{"type":22,"tag":40,"props":216,"children":219},{"href":217,"rel":218},"https://microsoft.github.io/presidio/",[44],[220],{"type":27,"value":221},"Microsoft Presidio",{"type":27,"value":223},". It's MIT-licensed, fully self-hosted, no cloud calls, and mature. There are two pieces we use here: an ",{"type":22,"tag":225,"props":226,"children":228},"code",{"className":227},[],[229],{"type":27,"value":230},"AnalyzerEngine",{"type":27,"value":232}," finds entities (spaCy for named-entity recognition, plus regex and checksum logic for things like credit cards) and an ",{"type":22,"tag":225,"props":234,"children":236},{"className":235},[],[237],{"type":27,"value":238},"AnonymizerEngine",{"type":27,"value":240}," replaces them. It's not the only option, but it's an accessible starting point.",{"type":22,"tag":88,"props":242,"children":244},{"id":243},"setting-up-presidio",[245],{"type":27,"value":125},{"type":22,"tag":23,"props":247,"children":248},{},[249],{"type":27,"value":250},"Install the pieces and a spaCy model for the NER engine:",{"type":22,"tag":252,"props":253,"children":257},"pre",{"className":254,"code":255,"language":256,"meta":7,"style":7},"language-bash shiki shiki-themes github-dark github-dark","uv add presidio-analyzer presidio-anonymizer\nuv run python -m spacy download en_core_web_lg\n","bash",[258],{"type":22,"tag":225,"props":259,"children":260},{"__ignoreMap":7},[261,289],{"type":22,"tag":262,"props":263,"children":266},"span",{"class":264,"line":265},"line",1,[267,273,279,284],{"type":22,"tag":262,"props":268,"children":270},{"style":269},"--shiki-default:#B392F0;--shiki-dark:#B392F0",[271],{"type":27,"value":272},"uv",{"type":22,"tag":262,"props":274,"children":276},{"style":275},"--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF",[277],{"type":27,"value":278}," add",{"type":22,"tag":262,"props":280,"children":281},{"style":275},[282],{"type":27,"value":283}," presidio-analyzer",{"type":22,"tag":262,"props":285,"children":286},{"style":275},[287],{"type":27,"value":288}," presidio-anonymizer\n",{"type":22,"tag":262,"props":290,"children":292},{"class":264,"line":291},2,[293,297,302,307,313,318,323],{"type":22,"tag":262,"props":294,"children":295},{"style":269},[296],{"type":27,"value":272},{"type":22,"tag":262,"props":298,"children":299},{"style":275},[300],{"type":27,"value":301}," run",{"type":22,"tag":262,"props":303,"children":304},{"style":275},[305],{"type":27,"value":306}," python",{"type":22,"tag":262,"props":308,"children":310},{"style":309},"--shiki-default:#79B8FF;--shiki-dark:#79B8FF",[311],{"type":27,"value":312}," -m",{"type":22,"tag":262,"props":314,"children":315},{"style":275},[316],{"type":27,"value":317}," spacy",{"type":22,"tag":262,"props":319,"children":320},{"style":275},[321],{"type":27,"value":322}," download",{"type":22,"tag":262,"props":324,"children":325},{"style":275},[326],{"type":27,"value":327}," en_core_web_lg\n",{"type":22,"tag":23,"props":329,"children":330},{},[331],{"type":27,"value":332},"Build the two engines once, at module load. Construction is expensive; don't pay it per request.",{"type":22,"tag":252,"props":334,"children":337},{"className":335,"code":336,"language":15,"meta":7,"style":7},"language-python shiki shiki-themes github-dark github-dark","from presidio_analyzer import AnalyzerEngine\nfrom presidio_anonymizer import AnonymizerEngine\nfrom presidio_anonymizer.entities import OperatorConfig\n\nanalyzer = AnalyzerEngine()  # loads the spaCy model; the expensive part\nanonymizer = AnonymizerEngine()\n\n# Trim or extend to match your data. A shorter list means fewer\n# false positives and less latency.\nTARGET_ENTITIES = [\n    \"PERSON\",\n    \"EMAIL_ADDRESS\",\n    \"PHONE_NUMBER\",\n    \"US_SSN\",\n    \"CREDIT_CARD\",\n    \"IBAN_CODE\",\n]\n\ndef redact(text: str) -> str:\n    \"\"\"Detect target entities and replace each with a typed placeholder.\"\"\"\n    results = analyzer.analyze(\n        text=text,\n        entities=TARGET_ENTITIES,\n        language=\"en\",\n    )\n    # Default `replace` yields a typed tag, e.g. \u003CPERSON>, \u003CEMAIL_ADDRESS>.\n    anonymized = anonymizer.anonymize(\n        text=text,\n        analyzer_results=results,\n        operators={\n            \"DEFAULT\": OperatorConfig(\n                \"replace\", {\"new_value\": None}\n            ),\n        },\n    )\n    return anonymized.text\n",[338],{"type":22,"tag":225,"props":339,"children":340},{"__ignoreMap":7},[341,349,357,366,376,385,394,402,411,420,429,438,447,456,465,474,483,492,500,509,518,527,536,545,554,563,572,581,589,598,607,616,625,634,643,651],{"type":22,"tag":262,"props":342,"children":343},{"class":264,"line":265},[344],{"type":22,"tag":262,"props":345,"children":346},{},[347],{"type":27,"value":348},"from presidio_analyzer import AnalyzerEngine\n",{"type":22,"tag":262,"props":350,"children":351},{"class":264,"line":291},[352],{"type":22,"tag":262,"props":353,"children":354},{},[355],{"type":27,"value":356},"from presidio_anonymizer import AnonymizerEngine\n",{"type":22,"tag":262,"props":358,"children":360},{"class":264,"line":359},3,[361],{"type":22,"tag":262,"props":362,"children":363},{},[364],{"type":27,"value":365},"from presidio_anonymizer.entities import OperatorConfig\n",{"type":22,"tag":262,"props":367,"children":369},{"class":264,"line":368},4,[370],{"type":22,"tag":262,"props":371,"children":373},{"emptyLinePlaceholder":372},true,[374],{"type":27,"value":375},"\n",{"type":22,"tag":262,"props":377,"children":379},{"class":264,"line":378},5,[380],{"type":22,"tag":262,"props":381,"children":382},{},[383],{"type":27,"value":384},"analyzer = AnalyzerEngine()  # loads the spaCy model; the expensive part\n",{"type":22,"tag":262,"props":386,"children":388},{"class":264,"line":387},6,[389],{"type":22,"tag":262,"props":390,"children":391},{},[392],{"type":27,"value":393},"anonymizer = AnonymizerEngine()\n",{"type":22,"tag":262,"props":395,"children":397},{"class":264,"line":396},7,[398],{"type":22,"tag":262,"props":399,"children":400},{"emptyLinePlaceholder":372},[401],{"type":27,"value":375},{"type":22,"tag":262,"props":403,"children":405},{"class":264,"line":404},8,[406],{"type":22,"tag":262,"props":407,"children":408},{},[409],{"type":27,"value":410},"# Trim or extend to match your data. A shorter list means fewer\n",{"type":22,"tag":262,"props":412,"children":414},{"class":264,"line":413},9,[415],{"type":22,"tag":262,"props":416,"children":417},{},[418],{"type":27,"value":419},"# false positives and less latency.\n",{"type":22,"tag":262,"props":421,"children":423},{"class":264,"line":422},10,[424],{"type":22,"tag":262,"props":425,"children":426},{},[427],{"type":27,"value":428},"TARGET_ENTITIES = [\n",{"type":22,"tag":262,"props":430,"children":432},{"class":264,"line":431},11,[433],{"type":22,"tag":262,"props":434,"children":435},{},[436],{"type":27,"value":437},"    \"PERSON\",\n",{"type":22,"tag":262,"props":439,"children":441},{"class":264,"line":440},12,[442],{"type":22,"tag":262,"props":443,"children":444},{},[445],{"type":27,"value":446},"    \"EMAIL_ADDRESS\",\n",{"type":22,"tag":262,"props":448,"children":450},{"class":264,"line":449},13,[451],{"type":22,"tag":262,"props":452,"children":453},{},[454],{"type":27,"value":455},"    \"PHONE_NUMBER\",\n",{"type":22,"tag":262,"props":457,"children":459},{"class":264,"line":458},14,[460],{"type":22,"tag":262,"props":461,"children":462},{},[463],{"type":27,"value":464},"    \"US_SSN\",\n",{"type":22,"tag":262,"props":466,"children":468},{"class":264,"line":467},15,[469],{"type":22,"tag":262,"props":470,"children":471},{},[472],{"type":27,"value":473},"    \"CREDIT_CARD\",\n",{"type":22,"tag":262,"props":475,"children":477},{"class":264,"line":476},16,[478],{"type":22,"tag":262,"props":479,"children":480},{},[481],{"type":27,"value":482},"    \"IBAN_CODE\",\n",{"type":22,"tag":262,"props":484,"children":486},{"class":264,"line":485},17,[487],{"type":22,"tag":262,"props":488,"children":489},{},[490],{"type":27,"value":491},"]\n",{"type":22,"tag":262,"props":493,"children":495},{"class":264,"line":494},18,[496],{"type":22,"tag":262,"props":497,"children":498},{"emptyLinePlaceholder":372},[499],{"type":27,"value":375},{"type":22,"tag":262,"props":501,"children":503},{"class":264,"line":502},19,[504],{"type":22,"tag":262,"props":505,"children":506},{},[507],{"type":27,"value":508},"def redact(text: str) -> str:\n",{"type":22,"tag":262,"props":510,"children":512},{"class":264,"line":511},20,[513],{"type":22,"tag":262,"props":514,"children":515},{},[516],{"type":27,"value":517},"    \"\"\"Detect target entities and replace each with a typed placeholder.\"\"\"\n",{"type":22,"tag":262,"props":519,"children":521},{"class":264,"line":520},21,[522],{"type":22,"tag":262,"props":523,"children":524},{},[525],{"type":27,"value":526},"    results = analyzer.analyze(\n",{"type":22,"tag":262,"props":528,"children":530},{"class":264,"line":529},22,[531],{"type":22,"tag":262,"props":532,"children":533},{},[534],{"type":27,"value":535},"        text=text,\n",{"type":22,"tag":262,"props":537,"children":539},{"class":264,"line":538},23,[540],{"type":22,"tag":262,"props":541,"children":542},{},[543],{"type":27,"value":544},"        entities=TARGET_ENTITIES,\n",{"type":22,"tag":262,"props":546,"children":548},{"class":264,"line":547},24,[549],{"type":22,"tag":262,"props":550,"children":551},{},[552],{"type":27,"value":553},"        language=\"en\",\n",{"type":22,"tag":262,"props":555,"children":557},{"class":264,"line":556},25,[558],{"type":22,"tag":262,"props":559,"children":560},{},[561],{"type":27,"value":562},"    )\n",{"type":22,"tag":262,"props":564,"children":566},{"class":264,"line":565},26,[567],{"type":22,"tag":262,"props":568,"children":569},{},[570],{"type":27,"value":571},"    # Default `replace` yields a typed tag, e.g. \u003CPERSON>, \u003CEMAIL_ADDRESS>.\n",{"type":22,"tag":262,"props":573,"children":575},{"class":264,"line":574},27,[576],{"type":22,"tag":262,"props":577,"children":578},{},[579],{"type":27,"value":580},"    anonymized = anonymizer.anonymize(\n",{"type":22,"tag":262,"props":582,"children":584},{"class":264,"line":583},28,[585],{"type":22,"tag":262,"props":586,"children":587},{},[588],{"type":27,"value":535},{"type":22,"tag":262,"props":590,"children":592},{"class":264,"line":591},29,[593],{"type":22,"tag":262,"props":594,"children":595},{},[596],{"type":27,"value":597},"        analyzer_results=results,\n",{"type":22,"tag":262,"props":599,"children":601},{"class":264,"line":600},30,[602],{"type":22,"tag":262,"props":603,"children":604},{},[605],{"type":27,"value":606},"        operators={\n",{"type":22,"tag":262,"props":608,"children":610},{"class":264,"line":609},31,[611],{"type":22,"tag":262,"props":612,"children":613},{},[614],{"type":27,"value":615},"            \"DEFAULT\": OperatorConfig(\n",{"type":22,"tag":262,"props":617,"children":619},{"class":264,"line":618},32,[620],{"type":22,"tag":262,"props":621,"children":622},{},[623],{"type":27,"value":624},"                \"replace\", {\"new_value\": None}\n",{"type":22,"tag":262,"props":626,"children":628},{"class":264,"line":627},33,[629],{"type":22,"tag":262,"props":630,"children":631},{},[632],{"type":27,"value":633},"            ),\n",{"type":22,"tag":262,"props":635,"children":637},{"class":264,"line":636},34,[638],{"type":22,"tag":262,"props":639,"children":640},{},[641],{"type":27,"value":642},"        },\n",{"type":22,"tag":262,"props":644,"children":646},{"class":264,"line":645},35,[647],{"type":22,"tag":262,"props":648,"children":649},{},[650],{"type":27,"value":562},{"type":22,"tag":262,"props":652,"children":654},{"class":264,"line":653},36,[655],{"type":22,"tag":262,"props":656,"children":657},{},[658],{"type":27,"value":659},"    return anonymized.text\n",{"type":22,"tag":23,"props":661,"children":662},{},[663,665,671,673,679,681,687],{"type":27,"value":664},"With Presidio's default ",{"type":22,"tag":225,"props":666,"children":668},{"className":667},[],[669],{"type":27,"value":670},"replace",{"type":27,"value":672}," operator, ",{"type":22,"tag":225,"props":674,"children":676},{"className":675},[],[677],{"type":27,"value":678},"Call Jane at jane@acme.com",{"type":27,"value":680}," becomes ",{"type":22,"tag":225,"props":682,"children":684},{"className":683},[],[685],{"type":27,"value":686},"Call \u003CPERSON> at \u003CEMAIL_ADDRESS>",{"type":27,"value":688},". The model can still read the sentence, it just can't read the PII.",{"type":22,"tag":88,"props":690,"children":692},{"id":691},"the-before-model-callback",[693],{"type":27,"value":134},{"type":22,"tag":23,"props":695,"children":696},{},[697,699,705,707,713,715,721],{"type":27,"value":698},"Now wire it into ADK. The callback receives a ",{"type":22,"tag":225,"props":700,"children":702},{"className":701},[],[703],{"type":27,"value":704},"CallbackContext",{"type":27,"value":706}," and the assembled ",{"type":22,"tag":225,"props":708,"children":710},{"className":709},[],[711],{"type":27,"value":712},"LlmRequest",{"type":27,"value":714},", mutates that request in place, and returns ",{"type":22,"tag":225,"props":716,"children":718},{"className":717},[],[719],{"type":27,"value":720},"None",{"type":27,"value":722}," to let the now-redacted request proceed.",{"type":22,"tag":252,"props":724,"children":726},{"className":335,"code":725,"language":15,"meta":7,"style":7},"from typing import Optional\nfrom google.adk.agents.callback_context import CallbackContext\nfrom google.adk.models import LlmRequest, LlmResponse\n\ndef redact_before_model(\n    callback_context: CallbackContext,\n    llm_request: LlmRequest,\n) -> Optional[LlmResponse]:\n    \"\"\"\n    Redact PII from every user turn before the model is called.\n    \"\"\"\n    for content in llm_request.contents:\n        if content.role != \"user\" or not content.parts:\n            continue\n        for part in content.parts:\n            if getattr(part, \"text\", None):\n                part.text = redact(part.text)\n\n    # Returning None tells ADK to proceed with the modified request.\n    return None\n",[727],{"type":22,"tag":225,"props":728,"children":729},{"__ignoreMap":7},[730,738,746,754,761,769,777,785,793,801,809,816,824,832,840,848,856,864,871,879],{"type":22,"tag":262,"props":731,"children":732},{"class":264,"line":265},[733],{"type":22,"tag":262,"props":734,"children":735},{},[736],{"type":27,"value":737},"from typing import Optional\n",{"type":22,"tag":262,"props":739,"children":740},{"class":264,"line":291},[741],{"type":22,"tag":262,"props":742,"children":743},{},[744],{"type":27,"value":745},"from google.adk.agents.callback_context import CallbackContext\n",{"type":22,"tag":262,"props":747,"children":748},{"class":264,"line":359},[749],{"type":22,"tag":262,"props":750,"children":751},{},[752],{"type":27,"value":753},"from google.adk.models import LlmRequest, LlmResponse\n",{"type":22,"tag":262,"props":755,"children":756},{"class":264,"line":368},[757],{"type":22,"tag":262,"props":758,"children":759},{"emptyLinePlaceholder":372},[760],{"type":27,"value":375},{"type":22,"tag":262,"props":762,"children":763},{"class":264,"line":378},[764],{"type":22,"tag":262,"props":765,"children":766},{},[767],{"type":27,"value":768},"def redact_before_model(\n",{"type":22,"tag":262,"props":770,"children":771},{"class":264,"line":387},[772],{"type":22,"tag":262,"props":773,"children":774},{},[775],{"type":27,"value":776},"    callback_context: CallbackContext,\n",{"type":22,"tag":262,"props":778,"children":779},{"class":264,"line":396},[780],{"type":22,"tag":262,"props":781,"children":782},{},[783],{"type":27,"value":784},"    llm_request: LlmRequest,\n",{"type":22,"tag":262,"props":786,"children":787},{"class":264,"line":404},[788],{"type":22,"tag":262,"props":789,"children":790},{},[791],{"type":27,"value":792},") -> Optional[LlmResponse]:\n",{"type":22,"tag":262,"props":794,"children":795},{"class":264,"line":413},[796],{"type":22,"tag":262,"props":797,"children":798},{},[799],{"type":27,"value":800},"    \"\"\"\n",{"type":22,"tag":262,"props":802,"children":803},{"class":264,"line":422},[804],{"type":22,"tag":262,"props":805,"children":806},{},[807],{"type":27,"value":808},"    Redact PII from every user turn before the model is called.\n",{"type":22,"tag":262,"props":810,"children":811},{"class":264,"line":431},[812],{"type":22,"tag":262,"props":813,"children":814},{},[815],{"type":27,"value":800},{"type":22,"tag":262,"props":817,"children":818},{"class":264,"line":440},[819],{"type":22,"tag":262,"props":820,"children":821},{},[822],{"type":27,"value":823},"    for content in llm_request.contents:\n",{"type":22,"tag":262,"props":825,"children":826},{"class":264,"line":449},[827],{"type":22,"tag":262,"props":828,"children":829},{},[830],{"type":27,"value":831},"        if content.role != \"user\" or not content.parts:\n",{"type":22,"tag":262,"props":833,"children":834},{"class":264,"line":458},[835],{"type":22,"tag":262,"props":836,"children":837},{},[838],{"type":27,"value":839},"            continue\n",{"type":22,"tag":262,"props":841,"children":842},{"class":264,"line":467},[843],{"type":22,"tag":262,"props":844,"children":845},{},[846],{"type":27,"value":847},"        for part in content.parts:\n",{"type":22,"tag":262,"props":849,"children":850},{"class":264,"line":476},[851],{"type":22,"tag":262,"props":852,"children":853},{},[854],{"type":27,"value":855},"            if getattr(part, \"text\", None):\n",{"type":22,"tag":262,"props":857,"children":858},{"class":264,"line":485},[859],{"type":22,"tag":262,"props":860,"children":861},{},[862],{"type":27,"value":863},"                part.text = redact(part.text)\n",{"type":22,"tag":262,"props":865,"children":866},{"class":264,"line":494},[867],{"type":22,"tag":262,"props":868,"children":869},{"emptyLinePlaceholder":372},[870],{"type":27,"value":375},{"type":22,"tag":262,"props":872,"children":873},{"class":264,"line":502},[874],{"type":22,"tag":262,"props":875,"children":876},{},[877],{"type":27,"value":878},"    # Returning None tells ADK to proceed with the modified request.\n",{"type":22,"tag":262,"props":880,"children":881},{"class":264,"line":511},[882],{"type":22,"tag":262,"props":883,"children":884},{},[885],{"type":27,"value":886},"    return None\n",{"type":22,"tag":23,"props":888,"children":889},{},[890],{"type":27,"value":891},"Register it on the agent:",{"type":22,"tag":252,"props":893,"children":895},{"className":335,"code":894,"language":15,"meta":7,"style":7},"from google.adk.agents import LlmAgent\n\nagent = LlmAgent(\n    model=\"gemini-3.5-flash\",\n    name=\"support_agent\",\n    instruction=\"You help users resolve support tickets.\",\n    before_model_callback=redact_before_model,\n)\n",[896],{"type":22,"tag":225,"props":897,"children":898},{"__ignoreMap":7},[899,907,914,922,930,938,946,954],{"type":22,"tag":262,"props":900,"children":901},{"class":264,"line":265},[902],{"type":22,"tag":262,"props":903,"children":904},{},[905],{"type":27,"value":906},"from google.adk.agents import LlmAgent\n",{"type":22,"tag":262,"props":908,"children":909},{"class":264,"line":291},[910],{"type":22,"tag":262,"props":911,"children":912},{"emptyLinePlaceholder":372},[913],{"type":27,"value":375},{"type":22,"tag":262,"props":915,"children":916},{"class":264,"line":359},[917],{"type":22,"tag":262,"props":918,"children":919},{},[920],{"type":27,"value":921},"agent = LlmAgent(\n",{"type":22,"tag":262,"props":923,"children":924},{"class":264,"line":368},[925],{"type":22,"tag":262,"props":926,"children":927},{},[928],{"type":27,"value":929},"    model=\"gemini-3.5-flash\",\n",{"type":22,"tag":262,"props":931,"children":932},{"class":264,"line":378},[933],{"type":22,"tag":262,"props":934,"children":935},{},[936],{"type":27,"value":937},"    name=\"support_agent\",\n",{"type":22,"tag":262,"props":939,"children":940},{"class":264,"line":387},[941],{"type":22,"tag":262,"props":942,"children":943},{},[944],{"type":27,"value":945},"    instruction=\"You help users resolve support tickets.\",\n",{"type":22,"tag":262,"props":947,"children":948},{"class":264,"line":396},[949],{"type":22,"tag":262,"props":950,"children":951},{},[952],{"type":27,"value":953},"    before_model_callback=redact_before_model,\n",{"type":22,"tag":262,"props":955,"children":956},{"class":264,"line":404},[957],{"type":22,"tag":262,"props":958,"children":959},{},[960],{"type":27,"value":961},")\n",{"type":22,"tag":23,"props":963,"children":964},{},[965],{"type":27,"value":966},"Now for every model call, the input passes through Presidio first. Raw PII never reaches the provider, the trace exporter, or anything downstream.",{"type":22,"tag":23,"props":968,"children":969},{},[970,972,982,984,990],{"type":27,"value":971},"That's one crack sealed, but there are others. Tools are their own leakage path, and the before-model callback doesn't touch them. ADK exposes a parallel hook, ",{"type":22,"tag":40,"props":973,"children":975},{"href":203,"rel":974},[44],[976],{"type":22,"tag":225,"props":977,"children":979},{"className":978},[],[980],{"type":27,"value":981},"before_tool_callback",{"type":27,"value":983},", that fires before a tool runs and can inspect and rewrite the tool's arguments. The same ",{"type":22,"tag":225,"props":985,"children":987},{"className":986},[],[988],{"type":27,"value":989},"redact",{"type":27,"value":991}," function drops in there to scrub PII out of tool inputs before they hit an external API or get logged by the tool. If your agent passes raw user text into tools, wire up both.",{"type":22,"tag":88,"props":993,"children":995},{"id":994},"but-what-if-a-tool-needs-pii",[996],{"type":27,"value":143},{"type":22,"tag":23,"props":998,"children":999},{},[1000,1002,1008],{"type":27,"value":1001},"So far so good, but software is rarely that simple. Redaction breaks an obvious thing: a user-lookup tool can't query for ",{"type":22,"tag":225,"props":1003,"children":1005},{"className":1004},[],[1006],{"type":27,"value":1007},"\u003CEMAIL_ADDRESS>",{"type":27,"value":1009},". When your tools act on the PII you just stripped, you need it back, but only inside the tool, never in the model.",{"type":22,"tag":23,"props":1011,"children":1012},{},[1013,1015,1021,1023,1028],{"type":27,"value":1014},"One clean pattern is a token vault. When you redact, give each entity a unique placeholder (",{"type":22,"tag":225,"props":1016,"children":1018},{"className":1017},[],[1019],{"type":27,"value":1020},"\u003CEMAIL_ADDRESS_1>",{"type":27,"value":1022},", not a bare ",{"type":22,"tag":225,"props":1024,"children":1026},{"className":1025},[],[1027],{"type":27,"value":1007},{"type":27,"value":1029},") and keep a per-request map from placeholder to real value. The map lives outside the prompt, and never gets logged. The model reasons over placeholders; the real values take a detour around it.",{"type":22,"tag":23,"props":1031,"children":1032},{},[1033,1035,1040],{"type":27,"value":1034},"When the model calls a tool, ",{"type":22,"tag":225,"props":1036,"children":1038},{"className":1037},[],[1039],{"type":27,"value":981},{"type":27,"value":1041}," swaps the placeholders back, just before execution:",{"type":22,"tag":252,"props":1043,"children":1045},{"className":335,"code":1044,"language":15,"meta":7,"style":7},"def rehydrate_before_tool(tool, args, tool_context):\n    \"\"\"Restore real values for a trusted tool, server-side, just in time.\"\"\"\n    # vault e.g. {\"\u003CEMAIL_ADDRESS_1>\": \"jane@acme.com\"}, kept in session state\n    vault = tool_context.state.get(\"pii_vault\", {})\n    for key, value in args.items():\n        if isinstance(value, str):\n            for token, real in vault.items():\n                value = value.replace(token, real)\n            args[key] = value\n    return None\n",[1046],{"type":22,"tag":225,"props":1047,"children":1048},{"__ignoreMap":7},[1049,1057,1065,1073,1081,1089,1097,1105,1113,1121],{"type":22,"tag":262,"props":1050,"children":1051},{"class":264,"line":265},[1052],{"type":22,"tag":262,"props":1053,"children":1054},{},[1055],{"type":27,"value":1056},"def rehydrate_before_tool(tool, args, tool_context):\n",{"type":22,"tag":262,"props":1058,"children":1059},{"class":264,"line":291},[1060],{"type":22,"tag":262,"props":1061,"children":1062},{},[1063],{"type":27,"value":1064},"    \"\"\"Restore real values for a trusted tool, server-side, just in time.\"\"\"\n",{"type":22,"tag":262,"props":1066,"children":1067},{"class":264,"line":359},[1068],{"type":22,"tag":262,"props":1069,"children":1070},{},[1071],{"type":27,"value":1072},"    # vault e.g. {\"\u003CEMAIL_ADDRESS_1>\": \"jane@acme.com\"}, kept in session state\n",{"type":22,"tag":262,"props":1074,"children":1075},{"class":264,"line":368},[1076],{"type":22,"tag":262,"props":1077,"children":1078},{},[1079],{"type":27,"value":1080},"    vault = tool_context.state.get(\"pii_vault\", {})\n",{"type":22,"tag":262,"props":1082,"children":1083},{"class":264,"line":378},[1084],{"type":22,"tag":262,"props":1085,"children":1086},{},[1087],{"type":27,"value":1088},"    for key, value in args.items():\n",{"type":22,"tag":262,"props":1090,"children":1091},{"class":264,"line":387},[1092],{"type":22,"tag":262,"props":1093,"children":1094},{},[1095],{"type":27,"value":1096},"        if isinstance(value, str):\n",{"type":22,"tag":262,"props":1098,"children":1099},{"class":264,"line":396},[1100],{"type":22,"tag":262,"props":1101,"children":1102},{},[1103],{"type":27,"value":1104},"            for token, real in vault.items():\n",{"type":22,"tag":262,"props":1106,"children":1107},{"class":264,"line":404},[1108],{"type":22,"tag":262,"props":1109,"children":1110},{},[1111],{"type":27,"value":1112},"                value = value.replace(token, real)\n",{"type":22,"tag":262,"props":1114,"children":1115},{"class":264,"line":413},[1116],{"type":22,"tag":262,"props":1117,"children":1118},{},[1119],{"type":27,"value":1120},"            args[key] = value\n",{"type":22,"tag":262,"props":1122,"children":1123},{"class":264,"line":422},[1124],{"type":22,"tag":262,"props":1125,"children":1126},{},[1127],{"type":27,"value":886},{"type":22,"tag":23,"props":1129,"children":1130},{},[1131],{"type":27,"value":1132},"The tool gets the real email, runs the lookup, and the model is never the wiser. Redact the tool's output on the way back, too, so the result doesn't smuggle PII into the next turn.",{"type":22,"tag":23,"props":1134,"children":1135},{},[1136],{"type":27,"value":1137},"Two rules:",{"type":22,"tag":1139,"props":1140,"children":1141},"ol",{},[1142,1153],{"type":22,"tag":99,"props":1143,"children":1144},{},[1145,1151],{"type":22,"tag":1146,"props":1147,"children":1148},"strong",{},[1149],{"type":27,"value":1150},"Restore PII only for tools inside your trust boundary.",{"type":27,"value":1152}," A tool that forwards data to a third party should get the redacted form, the same as the model does.",{"type":22,"tag":99,"props":1154,"children":1155},{},[1156,1161],{"type":22,"tag":1146,"props":1157,"children":1158},{},[1159],{"type":27,"value":1160},"Keep the vault ephemeral.",{"type":27,"value":1162}," It should be request-scoped, in-memory, cleared when the turn ends, never written to a log or trace. The point is that the PII lives in exactly one place you control, for exactly as long as you need it.",{"type":22,"tag":88,"props":1164,"children":1166},{"id":1165},"tradeoffs",[1167],{"type":27,"value":152},{"type":22,"tag":23,"props":1169,"children":1170},{},[1171],{"type":27,"value":1172},"This is one layer, not a complete solution.",{"type":22,"tag":23,"props":1174,"children":1175},{},[1176,1181],{"type":22,"tag":1146,"props":1177,"children":1178},{},[1179],{"type":27,"value":1180},"Detection is imperfect.",{"type":27,"value":1182}," Emails, SSNs, and credit card numbers are caught by regex and checksums, and those are reliable. Names and locations come from the NER model, and that's where the misses live. Will will write his will?. A false negative leaks; a false positive corrupts the input and can degrade the answer.",{"type":22,"tag":23,"props":1184,"children":1185},{},[1186,1191],{"type":22,"tag":1146,"props":1187,"children":1188},{},[1189],{"type":27,"value":1190},"It adds latency.",{"type":27,"value":1192}," With the large spaCy model, expect roughly tens of milliseconds on short inputs, scaling with text length, all on CPU. Usually negligible next to model latency, but it isn't free. Keep the entity list tight: each enabled recognizer costs time.",{"type":22,"tag":23,"props":1194,"children":1195},{},[1196,1201],{"type":22,"tag":1146,"props":1197,"children":1198},{},[1199],{"type":27,"value":1200},"It doesn't replace output scanning or log hygiene.",{"type":27,"value":1202}," The model can still emit sensitive content in its response, so you want output-side checks too. Your logs still need retention and access controls. Redaction at the agent level doesn't seal the system.",{"type":22,"tag":23,"props":1204,"children":1205},{},[1206,1211],{"type":22,"tag":1146,"props":1207,"children":1208},{},[1209],{"type":27,"value":1210},"The vault is a concentration risk.",{"type":27,"value":1212}," If you use the token-vault pattern, notice what you've built: one structure holding every real value for the request. That is the opposite of data minimization, so keep it ephemeral. In memory, request-scoped, cleared at the end of the turn, and never written to persistent stores. Persist the vault and you've rebuilt the leak with a map attached.",{"type":22,"tag":23,"props":1214,"children":1215},{},[1216,1221],{"type":22,"tag":1146,"props":1217,"children":1218},{},[1219],{"type":27,"value":1220},"Design for containment.",{"type":27,"value":1222}," Assume any one layer fails and ask what happens next. Input redaction's value is the direction it fails in: toward data minimization. When it works, the data was never there to leak.",{"type":22,"tag":88,"props":1224,"children":1226},{"id":1225},"conclusion",[1227],{"type":27,"value":161},{"type":22,"tag":23,"props":1229,"children":1230},{},[1231],{"type":27,"value":1232},"None of this is easy, and the more complex your system the harder PII containment will be. But this approach will at least act as a good first pass at the problem.",{"type":22,"tag":23,"props":1234,"children":1235},{},[1236,1238,1245,1247,1254,1256,1267,1269,1276],{"type":27,"value":1237},"It's worth noting that Presidio isn't your only choice. ",{"type":22,"tag":40,"props":1239,"children":1242},{"href":1240,"rel":1241},"https://protectai.com/llm-guard",[44],[1243],{"type":27,"value":1244},"LLM Guard",{"type":27,"value":1246}," wraps it with a broader set of scanners. ",{"type":22,"tag":40,"props":1248,"children":1251},{"href":1249,"rel":1250},"https://docs.litellm.ai/docs/proxy/guardrails/pii_masking_v2",[44],[1252],{"type":27,"value":1253},"LiteLLM",{"type":27,"value":1255}," ships a Presidio-based PII step you can put in front of any provider. For lighter needs, ",{"type":22,"tag":40,"props":1257,"children":1260},{"href":1258,"rel":1259},"https://scrubadub.readthedocs.io/",[44],[1261],{"type":22,"tag":225,"props":1262,"children":1264},{"className":1263},[],[1265],{"type":27,"value":1266},"scrubadub",{"type":27,"value":1268}," brings its own regex-based detection. Back when I was building an agent platform at Intuit, we used ",{"type":22,"tag":40,"props":1270,"children":1273},{"href":1271,"rel":1272},"https://docs.cloud.google.com/dialogflow/cx/docs/concept/security-settings",[44],[1274],{"type":27,"value":1275},"Dialogflow's built-in PII redaction",{"type":27,"value":1277},".",{"type":22,"tag":23,"props":1279,"children":1280},{},[1281],{"type":27,"value":1282},"Nothing here is specific to ADK. The key is to find the lifecycle hook that fires just before the model call, and put a redaction pass there. LangChain, LlamaIndex, and most serious agent frameworks have something similar.",{"type":22,"tag":23,"props":1284,"children":1285},{},[1286],{"type":27,"value":1287},"The mindset is the part worth keeping. We spend a lot of energy hardening what the model says. Spend some on what it's allowed to read. The cheapest data protection is the data that never entered the system in the first place.",{"type":22,"tag":88,"props":1289,"children":1291},{"id":1290},"further-reading",[1292],{"type":27,"value":170},{"type":22,"tag":95,"props":1294,"children":1295},{},[1296,1316,1365],{"type":22,"tag":99,"props":1297,"children":1298},{},[1299,1305,1307,1314],{"type":22,"tag":40,"props":1300,"children":1302},{"href":203,"rel":1301},[44],[1303],{"type":27,"value":1304},"ADK callbacks: types and signatures",{"type":27,"value":1306}," and ",{"type":22,"tag":40,"props":1308,"children":1311},{"href":1309,"rel":1310},"https://adk.dev/callbacks/design-patterns-and-best-practices/",[44],[1312],{"type":27,"value":1313},"callback design patterns",{"type":27,"value":1315},", including guardrail examples.",{"type":22,"tag":99,"props":1317,"children":1318},{},[1319,1326,1328,1333,1335,1341,1342,1348,1349,1355,1357,1364],{"type":22,"tag":40,"props":1320,"children":1323},{"href":1321,"rel":1322},"https://microsoft.github.io/presidio/anonymizer/",[44],[1324],{"type":27,"value":1325},"Presidio anonymizer operators",{"type":27,"value":1327}," (",{"type":22,"tag":225,"props":1329,"children":1331},{"className":1330},[],[1332],{"type":27,"value":670},{"type":27,"value":1334},", ",{"type":22,"tag":225,"props":1336,"children":1338},{"className":1337},[],[1339],{"type":27,"value":1340},"mask",{"type":27,"value":1334},{"type":22,"tag":225,"props":1343,"children":1345},{"className":1344},[],[1346],{"type":27,"value":1347},"hash",{"type":27,"value":1334},{"type":22,"tag":225,"props":1350,"children":1352},{"className":1351},[],[1353],{"type":27,"value":1354},"encrypt",{"type":27,"value":1356},") and ",{"type":22,"tag":40,"props":1358,"children":1361},{"href":1359,"rel":1360},"https://microsoft.github.io/presidio/analyzer/adding_recognizers/",[44],[1362],{"type":27,"value":1363},"adding custom recognizers",{"type":27,"value":1277},{"type":22,"tag":99,"props":1366,"children":1367},{},[1368,1374],{"type":22,"tag":40,"props":1369,"children":1371},{"href":42,"rel":1370},[44],[1372],{"type":27,"value":1373},"OWASP Top 10 for LLM Applications 2025",{"type":27,"value":1375},", LLM02 Sensitive Information Disclosure.",{"type":22,"tag":1377,"props":1378,"children":1379},"style",{},[1380],{"type":27,"value":1381},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":7,"searchDepth":359,"depth":359,"links":1383},[1384,1385,1386,1387,1388,1389,1390,1391,1392],{"id":90,"depth":291,"text":93},{"id":173,"depth":291,"text":107},{"id":188,"depth":291,"text":116},{"id":243,"depth":291,"text":125},{"id":691,"depth":291,"text":134},{"id":994,"depth":291,"text":143},{"id":1165,"depth":291,"text":152},{"id":1225,"depth":291,"text":161},{"id":1290,"depth":291,"text":170},"markdown","content:blog:pii-redaction-adk.md","content","blog/pii-redaction-adk.md","blog/pii-redaction-adk","md",1783435967724]