Redact arguments and results
Keep API keys and emails out of your logs. Mask card numbers before the model sees them.
flowchart LR
M["model's call<br/>email, api_key"] --> LH["LoggingHook<br/>args masked by key name"]
LH --> LOG[("logs")]
M --> T["tool runs<br/>real values"]
T --> MP["MaskCard.after_tool<br/>card to ****-****-****-4242"]
MP --> OUT["result to the model"]
@registry.tool
def lookup_customer(email: str, api_key: str) -> dict[str, str]:
"""Look up a customer's billing profile."""
card = "4242-4242-4242-4242"
return {"email": email, "card": card, "note": f"card {card} on file"}
A result-masking hook
class MaskCard(Hook):
"""Masks card numbers in tool results before the model or the caller sees them."""
async def after_tool(self, inv: ToolInvocation, result: ToolResult) -> ToolResult | None:
if not result.ok:
return None # keep failures as they are
return dataclasses.replace(result, value=_mask(result.value))
def _mask(value: Any) -> Any:
if isinstance(value, str):
return CARD.sub(r"****-****-****-\1", value)
if isinstance(value, dict):
return {key: _mask(item) for key, item in value.items()}
if isinstance(value, list):
return [_mask(item) for item in value]
return value
after_tool may return a new ToolResult; returning None keeps the old one.
Wire it
log = logging.getLogger("app.tools")
patterns = (*DEFAULT_REDACT, "email", "card") # key names to mask, case-insensitive
executor = Executor(registry, hooks=[LoggingHook(logger=log, redact=patterns), MaskCard()])
value = await executor.run("lookup_customer", {"email": "a@example.com", "api_key": "sk-123"})
print(value) # the model sees masked card numbers
print(lines.lines[0]) # tool.start: argument values masked by key name
print(redact({"Authorization": "Bearer x", "query": "returns"})) # for your own log lines
{'email': 'a@example.com', 'card': '****-****-****-4242', 'note': 'card ****-****-****-4242 on file'}
tool.start {"tool": "lookup_customer", "run_id": "run_...", "call_id": "call_...", "kind": "tool", "args": {"email": "***", "api_key": "***"}}
{'Authorization': '***', 'query': 'returns'}
What is already safe
| Surface |
Default |
LoggingHook |
masks keys containing key, token, secret, password, authorization, cookie; never logs result values or message content; withholds all arguments of confirm tools |
LoggingSink, OTelSink |
leave out arguments, results and messages unless capture_content=True |
| Error messages |
ToolArgsInvalid and ErrorInfo never hold input values; a crash's text is logged at DEBUG only |
Pitfalls
| Pitfall |
Do instead |
| Key matching is by name |
a secret under query is not masked: mask values in your own hook, as MaskCard does |
| The tool cache stores the raw result |
it is saved before after_tool hooks run: never cache such tools under cache_scope="global" |
Masking in after_tool hides data from your own code too |
callers of run get the masked value; keep raw data in the tool's own store if you need it |
Files
| File |
Does |
examples/cookbook_redact.py |
the tool, MaskCard, LoggingHook with extra patterns, redact() |
tests/docs/test_cookbook.py |
runs it offline and checks the output |