Stop an AI agent from exporting customer data
An agent that can export records or send email can send customer data to the wrong place. A prompt injection in a ticket asks for “a copy of the customer list”. A sales agent attaches a full export when one row was enough. The data leaves before anyone looks. That is data exfiltration, even when nobody intended it.
Most of what the agent does is fine and should not wait for anyone. The risk is concentrated in a few actions: exports, and email to addresses outside the company.
This guide checks those actions before they run. An external export waits for a person to approve it. A bulk external export is blocked. Email outside the company domain waits for approval. Internal email and everything else keep running.
How it works
Section titled “How it works”Before the export or the email, your code asks Vulnify about the action: the agent, EXPORT_DATA or SEND_EMAIL, the resource, the destination, and the record count. Vulnify answers ALLOW, REVIEW, or BLOCK. The action runs only when finalDecision is ALLOW. See Decisions.
destination is INTERNAL when the address is on your company domain and EXTERNAL_EMAIL otherwise. Use EXTERNAL_API when the data goes to another service. See Actions.
You need an API key from app.vulnify.io. Use a TEST key while you try this. Register the agent (SalesBot) and the resources (Customer Database, Customer Contacts) in the app under those names, or change the names in the code.
1. Guard the export and the email
Section titled “1. Guard the export and the email”The export tool waits up to 2 minutes for a person to approve a REVIEW, then runs only if it was approved. The email tool does not wait: a REVIEW stops the send and the model is told a person must approve. content is optional. Vulnify scans it for sensitive data and does not store it.
npm install @vulnify/sdkimport { Vulnify, VulnifyBlockedError } from '@vulnify/sdk';
const apiKey = process.env.VULNIFY_API_KEY;if (!apiKey) { throw new Error('Set VULNIFY_API_KEY');}
const vulnify = new Vulnify({ apiKey, baseUrl: process.env.VULNIFY_BASE_URL });
const COMPANY_DOMAIN = '@yourcompany.com';
function destinationFor(address: string): 'INTERNAL' | 'EXTERNAL_EMAIL' { return address.toLowerCase().endsWith(COMPANY_DOMAIN) ? 'INTERNAL' : 'EXTERNAL_EMAIL';}
/** Replace the bodies with your real export and email calls. */async function exportCustomers(rows: number, to: string): Promise<void> { console.log(`exporting ${rows} customer records to ${to}`);}async function sendEmail(to: string, subject: string, body: string): Promise<void> { console.log(`sending "${subject}" to ${to} (${body.length} chars)`);}
/** Export tool: on REVIEW, wait up to 2 minutes for a person to approve (in the app or in Slack). */async function exportCustomersTool(rows: number, to: string): Promise<string> { try { await vulnify.guard( { agent: 'SalesBot', action: 'EXPORT_DATA', resource: 'Customer Database', destination: destinationFor(to), recordsAffected: rows, }, () => exportCustomers(rows, to), { timeoutMs: 120_000, pollMs: 2000 }, ); return `Exported ${rows} customer records to ${to}.`; } catch (error) { if (error instanceof VulnifyBlockedError) { // BLOCK, or a review that was denied, expired, or not answered in time. return `Export not run. ${error.message} (event ${error.result.id ?? 'none'})`; } throw error; }}
/** Email tool: no wait. REVIEW stops the send and tells the model a person must approve. */async function emailCustomerTool(to: string, subject: string, body: string): Promise<string> { try { await vulnify.guard( { agent: 'SalesBot', action: 'SEND_EMAIL', resource: 'Customer Contacts', destination: destinationFor(to), recordsAffected: 1, content: body, // optional: scanned for sensitive data, not stored }, () => sendEmail(to, subject, body), ); return `Email sent to ${to}.`; } catch (error) { if (error instanceof VulnifyBlockedError) { return `Email not sent. ${error.message} (event ${error.result.id ?? 'none'})`; } throw error; }}
console.log(await emailCustomerTool('ana@example.com', 'Your renewal', 'Hi Ana, your plan renews next week.'));console.log(await exportCustomersTool(800, 'partner@example.com'));pip install vulnifyimport os
from vulnify import Vulnify, VulnifyBlockedError
api_key = os.environ.get("VULNIFY_API_KEY")if not api_key: raise SystemExit("Set VULNIFY_API_KEY")
vulnify = Vulnify(api_key=api_key)
COMPANY_DOMAIN = "@yourcompany.com"
def destination_for(address: str) -> str: return "INTERNAL" if address.lower().endswith(COMPANY_DOMAIN) else "EXTERNAL_EMAIL"
def export_customers(rows: int, to: str) -> None: """Replace with your real export. It runs only after ALLOW or an approval.""" print(f"exporting {rows} customer records to {to}")
def send_email(to: str, subject: str, body: str) -> None: """Replace with your real email call. It runs only after ALLOW.""" print(f'sending "{subject}" to {to} ({len(body)} chars)')
def export_customers_tool(rows: int, to: str) -> str: """On REVIEW, wait up to 2 minutes for a person to approve (in the app or in Slack).""" try: vulnify.guard( lambda: export_customers(rows, to), wait={"timeout": 120, "poll": 2}, agent="SalesBot", action="EXPORT_DATA", resource="Customer Database", destination=destination_for(to), records_affected=rows, ) return f"Exported {rows} customer records to {to}." except VulnifyBlockedError as blocked: # BLOCK, or a review that was denied, expired, or not answered in time. return f"Export not run. {blocked} (event {blocked.result.id or 'none'})"
def email_customer_tool(to: str, subject: str, body: str) -> str: """No wait: REVIEW stops the send and tells the model a person must approve.""" try: vulnify.guard( lambda: send_email(to, subject, body), agent="SalesBot", action="SEND_EMAIL", resource="Customer Contacts", destination=destination_for(to), records_affected=1, content=body, # optional: scanned for sensitive data, not stored ) return f"Email sent to {to}." except VulnifyBlockedError as blocked: return f"Email not sent. {blocked} (event {blocked.result.id or 'none'})"
print(email_customer_tool("ana@example.com", "Your renewal", "Hi Ana, your plan renews next week."))print(export_customers_tool(800, "partner@example.com"))From Claude, Cursor, Grok, or any MCP client connected to the Vulnify MCP server, the assistant records the decision with decide_action before it exports:
{ "agent": "SalesBot", "action": "EXPORT_DATA", "resource": "Customer Database", "destination": "EXTERNAL_EMAIL", "recordsAffected": 800}When finalDecision is REVIEW, it does not export. After a person approves, get_decision reads the result. waitMs polls for up to 30 seconds:
{ "id": "<event id from decide_action>", "waitMs": 30000 }The export may run only when get_decision returns finalDecision ALLOW. The MCP server has no approve tool. A person approves in Vulnify or in Slack.
2. Add the policy
Section titled “2. Add the policy”Three rules. External customer exports of up to 1000 records go to a person. External customer exports of more than 1000 records are blocked. Email to an external address goes to a person. The record ranges do not overlap, so only one export rule matches a given action.
resource: CUSTOMER_PII matches resources whose type is customer PII. Use ANY if you have not set resource types. SEND_EMAIL has no action family of its own, so that rule uses action: ANY and puts SEND_EMAIL in the condition. The format is on Policy files.
apiVersion: vulnify.io/v1kind: Policymetadata: name: review-external-customer-exportsspec: description: A person approves customer exports that leave the company, up to 1000 records enabled: true action: EXPORT resource: CUSTOMER_PII condition: destination: EXTERNAL maxRecords: 1000 decision: REVIEW mode: ENFORCE approverRoles: - OWNER - ADMIN---apiVersion: vulnify.io/v1kind: Policymetadata: name: block-bulk-external-customer-exportsspec: description: Customer exports of more than 1000 records never leave the company enabled: true action: EXPORT resource: CUSTOMER_PII condition: destination: EXTERNAL minRecords: 1001 decision: BLOCK mode: ENFORCE---apiVersion: vulnify.io/v1kind: Policymetadata: name: review-external-customer-emailspec: description: A person approves agent email to addresses outside the company enabled: true action: ANY resource: ANY condition: action: SEND_EMAIL destination: EXTERNAL decision: REVIEW mode: ENFORCESave it as vulnify/policies/exports.yaml, then check and apply it with the CLI:
npx -p @vulnify/sdk vulnify policies validatenpx -p @vulnify/sdk vulnify policies apply --dry-runnpx -p @vulnify/sdk vulnify policies applyapply needs an org-wide LIVE key. You can also create the rules in the app under Policies. Start with mode: MONITOR if you want to see what would be reviewed before you enforce it.
3. What REVIEW and BLOCK look like
Section titled “3. What REVIEW and BLOCK look like”These are real results from test runs, not made-up values.
An export of 800 records to an external email address, on a test resource, came back as REVIEW (fields trimmed):
{ "decision": "REVIEW", "finalDecision": "REVIEW", "riskLevel": "HIGH", "riskScore": 65, "reasons": ["Export operation", "External destination", "Large data volume (800 records)"], "dlpFindings": []}The export did not run. The Slack alert showed Approve and Deny. After Approve, get_decision returned finalDecision ALLOW, and only then could the export run. The stored decision stays REVIEW. See Reviews and Slack.
An export of 12,000 records from Customer Database to an external email address, with a TEST key and no policy, came back as BLOCK from the risk engine:
{ "decision": "BLOCK", "finalDecision": "BLOCK", "riskLevel": "CRITICAL", "riskScore": 100, "reasons": ["Export operation", "Customer PII", "External destination", "Large data volume (12,000 records)"], "policy": null}With the policy above applied, that export matches block-bulk-external-customer-exports and policy.name names it. Your scores and reasons depend on your resources and policies.
In the code above, the tool returns a string such as Export not run. Vulnify BLOCK: … to the model. The model can tell the user why. If Vulnify cannot be reached, the SDK fails closed by default and nothing is exported.
Next steps
Section titled “Next steps”- Run the export check end to end in the Quickstart.
- Connect Slack so approvers can answer from the alert: Slack.
- Guard deletes the same way: Stop an AI agent from deleting production data.
- Use the OpenAI Agents SDK or Vercel AI SDK adapters: Framework adapters.
- Create the policy and an API key in the Vulnify app.

