AI agent data minimization for safer tool calls
AI agent data minimization keeps customer records out of tool calls by enforcing narrow inputs, bounded outputs, and trusted execution boundaries.

An AI agent should not receive a customer record just because it may eventually need one fact from it. Every tool call needs a smaller contract: the action, the minimum fields required to carry it out, and an output that tells the agent what happened without handing it a fresh pile of customer data.
Teams usually leak data at the seam between a capable agent and a convenient internal API. They give the agent a general customer lookup, return the whole response, and call the result useful context. That choice turns every later prompt, transcript, retry, debug trace, and tool result into a place where the record can spread.
The fix is not a clever redaction prompt. It is a design discipline: map each action before exposing it, enforce a narrow request at the execution boundary, and make raw upstream responses unavailable to the agent.
A tool permission does not entitle an agent to the whole record
Permission to call an API and permission to see every field that API can return are different decisions. Teams blur them because a service account can often read a broad object, while an agent tool needs only a small part of it.
Consider an agent that must decide whether to send a payment reminder. The delivery service may need a recipient address, a template identifier, and an invoice reference. The agent itself may only need eligible: true, a safe display name, and an action reference. It does not need payment history, customer notes, tax information, or the address that the sender uses behind the scenes.
A broad get_customer tool creates an attractive nuisance. Once it exists, prompts start using it for unrelated jobs because it seems cheaper than making a proper action tool. People then compensate with instructions such as "do not expose sensitive fields". Instructions do not remove fields from a JSON response.
Keep these three questions separate:
- Can this agent process initiate this action?
- What fields must the executor receive to perform it?
- What facts must the agent receive after it finishes?
Each question should produce its own constraint. Access control answers the first one. Request validation answers the second. Response shaping answers the third. A single all-purpose API token and a flexible JSON endpoint answer none of them well.
This distinction matters most when an agent uses a tool repeatedly. A one-time broad lookup looks tolerable in a demo. In a real run, the agent may call it again after a retry, quote its output in a later request, or pass it to another tool. The first unnecessary field becomes many unnecessary copies.
Build the data map around an action, not a database table
A useful data map begins with a verb and an external effect. "Read customer" is not an action for this purpose. "Confirm whether an invoice is overdue" and "create a shipment label" are actions because each has a concrete recipient, purpose, and expected result.
For every proposed tool, write a small record before writing the schema:
| Item | Example: send payment reminder |
|---|---|
| Initiator | A billing-support agent process |
| Effect | Sends one approved template to an eligible recipient |
| Minimum input | invoice_ref, template_code |
| Trusted lookup | Recipient address, language preference, eligibility rules |
| Agent-visible result | sent, suppressed, or needs_human_review |
| Forbidden input | Email address, payment history, account notes, full customer object |
| Forbidden output | Delivery address, raw provider response, payment details |
The trusted lookup column does the difficult work. It identifies information the executor may need but the agent does not. Move that lookup behind the boundary. The agent sends an invoice reference, and a service under your control resolves the recipient only after it checks the action.
Do not make invoice_ref a disguised copy of an email address or a compound identifier containing a customer name. Opaque references reduce incidental disclosure, but they do not automatically make a system private. If a reference lets anyone query a customer object, it still needs authorization, expiration, and an audience restriction.
The European Union's General Data Protection Regulation states the relevant principle plainly in Article 5(1)(c): personal data must be adequate, relevant, and limited to what is necessary for the purpose. The phrase "for the purpose" is where engineering teams often get careless. The purpose is not "help the agent complete its work." It is the specific operation at the boundary, such as sending one reminder or opening one support case.
A map also exposes fields that should never cross the boundary in either direction. Free-text notes deserve their own row. They routinely hold pasted emails, identity documents, credentials, health information, and a customer's unfiltered complaint. A generic notes field has no bounded meaning, so it has no place in a routine tool request.
Strict schemas stop convenience fields before execution
A schema should reject fields that the action does not need. Silently ignoring extra properties sounds forgiving, but it hides a leak during testing and leaves developers believing the field made it through.
Suppose an agent needs to request a refund review. This request contract permits only a case reference and a selected reason. It rejects customer-controlled amounts, addresses, and arbitrary notes.
{
"name": "request_refund_review",
"description": "Create a review task for an existing support case.",
"input_schema": {
"type": "object",
"additionalProperties": false,
"required": ["case_ref", "reason_code"],
"properties": {
"case_ref": {
"type": "string",
"pattern": "^case_[A-Za-z0-9]{16}$"
},
"reason_code": {
"type": "string",
"enum": ["duplicate_charge", "service_not_received", "other"]
}
}
}
}
A request with customer_email, shipping_address, amount, or conversation_text should fail at validation with an explicit result such as:
{
"error": "invalid_request",
"message": "Unexpected property: customer_email"
}
That error tells the agent to use the contract, and it tells the developer that an unwanted field reached the boundary. Do not echo the rejected value in the error. Error handlers have caused more leaks than many production paths because they serialize the whole failed request for debugging.
Schema validation alone does not protect server-owned fields. A request may pass a valid-looking case_ref that belongs to another account, or a valid reason_code paired with an action reference issued to a different agent. The executor must bind the reference to its issuer, intended action, and lifetime. Think of an action reference as a claim ticket, not as a public database primary key.
Use a request builder when an agent starts with untrusted or overly broad context. The builder should extract the allowed fields and create a new object. Do not take a large object and delete a few known-dangerous keys. Denylist redaction fails whenever a new field appears, a nested object changes shape, or a developer calls the tool with a differently named alias.
ALLOWED_REASONS = {"duplicate_charge", "service_not_received", "other"}
def build_refund_request(case_ref, reason_code):
if not isinstance(case_ref, str) or not case_ref.startswith("case_"):
raise ValueError("invalid case_ref")
if reason_code not in ALLOWED_REASONS:
raise ValueError("invalid reason_code")
return {"case_ref": case_ref, "reason_code": reason_code}
This small function prevents a common failure: passing an entire case dictionary to a client library because the library accepts arbitrary keyword arguments. The explicit return object is boring. Boring is good at a customer-data boundary.
Tool output needs its own contract
Raw tool output is agent context, even if the tool never receives customer data as input. Treat every response as material the agent can quote, retain, transform, or send to another system.
A provider response after creating a shipment may include the recipient's full address, telephone number, carrier account information, label data, routing details, and internal diagnostic fields. The agent usually needs only a shipment reference and whether it can tell the customer that the order is on its way.
Define the agent-facing response separately from the service-facing result:
{
"status": "created",
"shipment_ref": "ship_Q7J4K2P8",
"customer_message_allowed": true
}
The execution service can store or pass the detailed carrier response where operational staff need it. It should not return that response because returning it is convenient for debugging. If an operator needs a receipt, give the operator a protected interface for it. Do not use the agent transcript as a troubleshooting database.
Error responses need the same treatment. An upstream API may return the rejected street address, an account number, or a quoted portion of the request. Convert it into a bounded error code for the agent, such as recipient_unavailable, reference_invalid, or provider_retryable. Put protected diagnostic details in a system intended for operators.
The Model Context Protocol specification defines tools as callable functions with structured inputs and results. That structure gives developers a clean place to enforce response types. A tool that returns a prose dump or arbitrary JSON gives up that advantage. A narrow response schema also makes agent behavior easier to test, because the next decision can only depend on known fields.
Avoid returning a field named details unless you can state its exact structure and sensitivity rules. A vague escape hatch becomes permanent. Someone will put the raw result there during an incident, then forget to remove it.
Redaction, pseudonyms, and secrecy are different controls
Replacing a name with a token does not mean the data has become safe to circulate. A stable token that can be joined to a customer database remains personal data in most practical threat models. A one-time action reference, limited to one caller and a short lifetime, has a much narrower failure mode.
Redaction removes known values from an object. It helps when an internal system must show a record to an operator, but it is brittle as the primary boundary for agents. Field names change. Content moves into nested structures. Free text contains information no fixed redaction list can reliably find.
Pseudonymization substitutes an identifier for a direct identifier. It reduces exposure when the recipient cannot resolve the mapping. It fails when the same agent can call a broad lookup tool with that identifier, when the token appears across unrelated tools, or when the value itself carries meaning. acme-health-urgent-001 is not opaque merely because it lacks an email sign.
Secrecy comes from keeping the resolving data and credentials on the trusted side of the boundary. The agent sends a constrained instruction, and a service resolves protected information only for that permitted action. This is the distinction that prevents the familiar mistake of passing a "sanitized" customer object to an agent and assuming the job is done.
You should also separate data minimization from authorization. A properly authorized agent can still receive too much data. Conversely, an unauthorized caller may receive very little, but that little could still cause harm. Enforce both controls, and test them independently.
Broad credentials make narrow schemas less credible
A perfect request schema cannot compensate for an agent holding a credential that can call the underlying API directly. If the agent can read the secret or use it from its runtime, it can bypass the carefully designed tool and ask the provider for a richer response.
Put credentials where the action executes, not where the model reasons. The executor injects the authorization header or SSH identity after it validates the narrow request. The agent sees the result, not the secret, a placeholder, or a copy in an environment variable.
OAuth 2.0, described in RFC 6749, uses scopes to limit the access granted to a client. Scope is useful, but many implementations treat a scope like a broad department pass. A customers.read scope may still permit a full-record lookup. Pair credential scope with action-specific endpoints and response filtering. Otherwise the scope only limits which large collection the agent can search.
Keep authority split by effect. An agent that may create a draft should not also get authority to send it. An agent that may request a refund review should not issue a refund. This separation reduces the pressure to add a general-purpose administrative credential just to get one workflow working.
For macOS teams using Sallyport, the app keeps API and SSH credentials in its encrypted vault and executes the HTTP or SSH action rather than disclosing the credential to the agent. That arrangement helps only if the call itself stays narrow; a protected token can still authorize an overbroad request.
A support workflow shows where data spreads
A common leak starts with a reasonable request: let a support agent prepare a response to a failed delivery. The first implementation exposes get_order(order_id), which returns the order, customer profile, delivery address, payment status, support history, and carrier events. The agent needs the carrier event and permission to send one approved update.
The agent calls the lookup and receives the full record. It then passes selected details to a message-drafting tool. The drafting prompt now contains the address and history even though neither affects the message. A failed tool call sends the whole prompt to an error log. An engineer copies that error into an issue for diagnosis. The original broad response has become four separate retention problems.
Build the workflow differently. Give the agent an assess_delivery_update action that accepts order_ref. The execution service verifies the caller's authority, fetches the order internally, reads carrier status, applies the contact rule, and returns only this:
{
"status": "contact_allowed",
"event_code": "delivery_delayed",
"approved_template": "delivery_delay_notice",
"order_ref": "ord_9VJ3R6M1"
}
The agent can choose whether the situation warrants the approved message. A separate send_approved_delivery_update action accepts order_ref and approved_template. It does not accept an email address or a free-text body. The service resolves the recipient and renders the template after it verifies consent and order state.
This design seems more restrictive because it is. That restriction is the point. The agent cannot casually repurpose a customer profile for a different task, and a later tool cannot receive the data by accident.
Do not counter this with a generic "tool needs flexibility" argument. Flexibility belongs inside trusted application code, where you can test it, review it, and audit its data handling. Giving flexibility to an agent through broad request and response objects transfers the cost to every prompt and every downstream system.
Approval screens cannot inspect every hidden field
Approval protects against unauthorized actions, but it cannot reliably police oversized payloads. A person sees a short summary, makes a judgment under time pressure, and approves the action. If the service hides an unnecessary address or history inside a request, the approval did nothing to minimize disclosure.
Approval also creates a bad incentive when it becomes the substitute for tool design. Developers keep adding fields because "the user approves every call." Soon the approval card contains too much detail to read, or too little detail to judge. People approve repeated benign-looking actions without noticing that one request includes a new field.
Show the action and its bounded parameters in an approval interface, but enforce the allowlist before that interface appears. The approver should choose whether to authorize send approved delivery update for ord_9VJ3R6M1, not whether to manually inspect a serialized customer record.
Use per-call approval for effects where each execution deserves human attention. Do not use it as a scanner for personal data. The person who sees the approval has neither the time nor the context to decide whether every nested field was necessary.
A good review test is simple: remove the approver from the story. Would the tool contract still prevent unnecessary data from leaving the trusted service? If the answer is no, the boundary is doing too little.
Audit records should prove actions without becoming another data store
You need evidence when an agent acted on a customer's behalf. You do not need a permanent, agent-readable archive of full payloads to get that evidence.
Record the action name, initiating process or session, timestamp, outcome, authorization decision, and a correlation reference. If you need integrity evidence for the payload, record a cryptographic digest of a canonical, protected representation rather than the representation itself. Store the field names that were accepted, not their sensitive values.
For example, an audit event might preserve this shape:
{
"action": "send_approved_delivery_update",
"session_ref": "sess_4KH8N2",
"order_ref_digest": "sha256:8e4c...",
"accepted_fields": ["order_ref", "approved_template"],
"outcome": "sent",
"authorized_by": "per_call"
}
A digest does not magically erase privacy risk. If the input comes from a small known set, an attacker can guess values and compare hashes. Use a protected internal reference when operators need to retrieve details, and limit access to the system that holds the original record. Never treat a plain hash of an email address as anonymous.
Keep operational diagnostics separate from the agent's own tool result. Support staff may need protected access to an upstream error body for a short period. The agent does not. That separation also gives you a cleaner deletion and retention story because raw records do not accumulate in every journal.
Sallyport projects agent sessions and individual calls from a write-blind encrypted, hash-chained audit log; its sp audit verify command can verify the chain offline without a vault key. Integrity verification answers whether a recorded event was altered. Your event schema still decides whether that record contains too much customer data in the first place.
Test the boundary with deliberate over-sharing
A privacy review that tests only valid happy-path calls misses the behavior that causes most accidental exposure. Test what happens when an agent submits a full object, when an upstream service returns an unexpected field, and when an exception occurs halfway through a request.
Use a fixture that contains recognizable fake sensitive values, then assert that they do not cross the agent boundary. The fixture should include nested fields and free text because flat examples give redaction code an easy pass.
{
"case_ref": "case_Ab92Kx71LmQ4Rt8P",
"reason_code": "duplicate_charge",
"customer": {
"email": "[email protected]",
"address": "17 Example Lane",
"payment_note": "card ending 4242"
},
"conversation_text": "Customer says their medical appointment depends on delivery."
}
The expected result is a validation failure that names only the unexpected property. Then inspect four places: the agent-visible response, application logs, error tracking records, and audit events. Developers often validate the request but forget that their exception middleware recorded the original body.
Add contract tests for output as well. Mock an upstream response that contains a full customer object and assert that the tool emits only its documented fields. Do this whenever the provider client changes. An SDK upgrade can add response fields without anyone touching the agent prompt.
Finally, run a transcript test. Give the agent a normal task, capture all tool inputs and results that the agent receives, and search for the fixture values. This test catches accidental prompt interpolation and debug text that schema tests may miss.
The first action to redesign is usually the broad lookup tool. Replace it with one action that makes a real external effect or returns one bounded decision. If the new contract feels too narrow to be convenient, that is often evidence that the system has been relying on the agent to carry data it never needed.
FAQ
What does data minimization mean for AI agent tool calls?
A tool call should carry only the fields required to perform that one action and return a useful result. It should not carry an entire customer record because that record happened to be available in the agent's context. The safe default is an action-specific request assembled by trusted code.
How do I decide which customer fields an AI agent actually needs?
Start with the tool action, not the data source. Write down the decision the external service must make, then list the exact fields it needs to make it. If you cannot explain why a field is present in one sentence, remove it and see whether the action still works.
Is redacting names and email addresses enough to protect customer data?
No. Removing obvious fields such as email addresses and phone numbers helps, but account IDs, invoice references, timestamps, locations, and free-text notes can still identify or expose a customer. Treat redaction as one control in a larger design that also limits inputs, output, access scope, and retention.
Should an AI agent receive customer IDs?
The agent needs a customer identifier only when the downstream action needs it to find or change a particular record. A stable opaque reference is usually better than a full profile or a human-readable identifier. Do not expose internal IDs merely for convenience if a trusted service can resolve a short-lived action reference instead.
Why is tool output a customer-data risk?
Tool output often leaks more than tool input because developers return the raw upstream response for convenience. Define a response contract that contains only the status and facts the agent needs for its next decision. Keep receipts and detailed records in the service or audit system, not in the agent transcript.
Can human approval make broad agent requests safe?
A human approval can stop an action, but it does not repair a request that already contains unnecessary data. The person approving a call may see a summary rather than every field, and approval fatigue makes detailed inspection unreliable. Reduce the payload before the approval boundary.
How should tools authenticate without giving secrets to the agent?
Use separate credentials or an action gateway that injects credentials after it validates a narrow request. Give a tool only the permissions needed for its action, and never place API keys or SSH keys in the agent context. Credentials restrict what a call can do; schemas restrict what data it carries, so you need both.
What should I log for an AI agent action?
Logs need enough information to prove which action occurred, who or what initiated it, when it happened, and whether it succeeded. They do not need full request bodies, raw tool outputs, or permanent copies of customer records. Store a digest, field allowlist, outcome, and a protected correlation reference instead.
Are free-text notes safe to send to an agent tool?
Free-text fields are high risk because they commonly contain details that no schema can predict, including names, addresses, health details, credentials, and pasted correspondence. Do not pass them through by default. Extract a narrowly defined fact with trusted code, or require a human review path when the text is needed.
How do I test whether an agent tool leaks customer data?
Test the boundary with intentionally oversized and malformed payloads. A good test proves that the tool rejects unknown fields, strips server-owned values, returns a narrow response, and leaves no sensitive values in agent-visible logs. Also test error paths, because exceptions often dump raw upstream bodies.