How to detect PHI in AI agent prompts and tool calls

PHI detection finds the identifiers that tie health information to a person, so a policy can block a prompt or tool call before an MRN or member ID leaves.

19 min read
In short: PHI detection finds the identifiers that tie health information to a person, such as a name, a medical record number, or a health plan member ID. Run it on each prompt, tool argument, and tool result, and block the tool call when an identifier appears where it doesn't belong. Arcjet detects fixed-format identifiers such as VINs from their structure and MRNs and member IDs from labels next to them, and maps its entity types to 14 of HIPAA's 18 Safe Harbor classes, some only in part. Detection doesn't redact or de-identify data, and it doesn't make a system HIPAA compliant on its own.

How do you detect PHI in AI agent prompts and tool calls?

PHI detection finds the identifiers that tie health information to a person, such as a name, a medical record number (MRN), or a health plan member ID, in the text an agent handles. Run it on each prompt, tool argument, and tool result before the text reaches a model, a log, or an outside system, and block the call when an identifier appears where it doesn't belong.

Agents make this harder than it is in a form-based app. A clinician pastes a discharge summary into a chat assistant. A support agent copies a member ID into a ticket that an AI tool summarizes into a shared channel. A coding agent reads a test fixture of real patient rows and sends it to a model provider. No person reads each step.

Arcjet is an AI agent runtime security platform that enforces policy on each prompt and tool call, in your code through guard() and in the hooks of coding agents. Its server-side sensitive information detector recognizes five PHI identifier types alongside 20 core types such as names and Social Security numbers. This article is technical guidance, not legal advice.

What makes health information PHI?

Protected health information (PHI) is health information that can be linked to a specific person. A diagnosis on its own is health information. The same diagnosis next to a name, an MRN, or a phone number is PHI, because the identifier says whose diagnosis it is. For a detector, the identifier is the part to look for.

The definitions are in 45 CFR 160.103. HIPAA's rules apply to covered entities, which are health plans, clearinghouses, and providers that conduct standard transactions, such as claims, electronically, and to business associates, the vendors that handle PHI on a covered entity's behalf. A business associate agreement (BAA) is the contract that sets how the associate may use the PHI and must protect it.

The practical list of identifiers comes from the de-identification rule. 45 CFR 164.514(b)(2) names 18 classes of identifier, of the person and of their relatives, employers, and household members, that must be removed before data counts as de-identified under the Safe Harbor method. HHS's de-identification guidance explains how to apply it. Regulation text in this article comes from the eCFR, checked on November 6, 2026.

How does PHI detection recognize an MRN or a member ID?

Identifiers with a fixed format are recognized from their structure. Identifiers with no fixed format, such as an MRN, a health plan member ID, or a license plate, look like any other short code, so a detector has to read the text around the value. A label such as MRN, Member ID, or License plate next to the value is what marks it as an identifier.

A vehicle identification number (VIN), an IMEI, and a MAC address each follow a published format. An MRN doesn't, because each hospital assigns its own, and member IDs vary by health plan. The string A10234987 could be a member ID, an order number, or a ticket reference. Arcjet reads the surrounding text for that reason: MRN: 00483921 is a medical record number, and Order 00483921 shipped isn't.

Any detector that relies on context can be wrong in both directions. A CSV whose mrn header sits rows above the values gives the detector little to go on, and an identifier can go unflagged. A loyalty number in a retail support ticket that mentions a "member" can be flagged as a member ID. Blocking that ticket breaks a workflow that has nothing to do with health data. Test against samples shaped like your own traffic before a rule blocks anything, as described in the rollout section.

Which of the 18 HIPAA identifiers can Arcjet detect?

Arcjet has entity types for 14 of the 18 Safe Harbor classes, three of them only in part. It has none for the other four: dates and ages, biometric identifiers, full-face photographs, and any other unique identifying number, characteristic, or code. An entity type is a kind of sensitive information that a detector can find, such as EMAIL.

The following table maps each class in 45 CFR 164.514(b)(2)(i) to Arcjet entity types and where each is detected:

Safe Harbor identifier classArcjet entity typesWhere detected
NamesGIVEN_NAME, SURNAMEYour process or server
Geographic subdivisions smaller than a state, including ZIP codes

BUILDING_NUMBER, STREET_NAME, SECONDARY_ADDRESS, CITY, ZIP_CODE (no county, precinct, or geocode)

Your process or server
Dates related to a person, other than the year, and ages over 89Unsupported–
Telephone numbersPHONE_NUMBERYour process or server
Fax numbersPHONE_NUMBERYour process or server
Email addressesEMAILYour process or server
Social Security numbersSSNYour process or server
Medical record numbersMEDICAL_RECORD_NUMBERServer only
Health plan beneficiary numbersHEALTH_PLAN_BENEFICIARY_NUMBERServer only
Account numbersBANK_ACCOUNT (bank accounts only)Your process or server
Certificate and license numbersDRIVERS_LICENSE (driver's licenses only)Your process or server
Vehicle identifiers and serial numbers, including license platesLICENSE_PLATE_NUMBER, VEHICLE_IDENTIFICATION_NUMBERServer only
Device identifiers and serial numbersDEVICE_IDENTIFIERServer only
Web URLsURLYour process or server
IP addressesIP_ADDRESSYour process or server
Biometric identifiers, including finger and voice printsUnsupported–
Full-face photographs and comparable imagesUnsupported–
Any other unique identifying number, characteristic, or codeUnsupported–

The unsupported rows need their own controls. Arcjet doesn't detect dates, so "admitted March 14, DOB 1934" passes with both dates and an age over 89 intact. Check a structured field such as a date of birth in your own code. Biometrics and photographs aren't text, and the catch-all class has no fixed list by design. Of the partial rows, the address types have no type for a county, a precinct, or a geocode such as map coordinates, BANK_ACCOUNT doesn't cover a hospital billing account, and DRIVERS_LICENSE doesn't cover a professional license. STATE isn't listed because Safe Harbor allows a state to remain.

Does removing all 18 identifiers make health data safe?

No. Safe Harbor has a second condition in 45 CFR 164.514(b)(2)(ii): the organization must not have actual knowledge that the remaining information could identify the person, alone or combined with other information. Clinical detail can do that with no name, number, or date in the text. A rare diagnosis or a revealing occupation can point to one patient.

HHS's guidance gives examples of actual knowledge involving occupation, family relationships, rare clinical events, and a recipient able to re-identify the data. It applies the same standard to free text such as discharge summaries and progress notes, and cautions that identifiers aren't always clearly labeled, as the Military Health System's summary of the guidance sets out.

Language models sharpen the problem. In a February 2026 preprint, Paradox of De-identification: A Critique of HIPAA Safe Harbour in the Age of LLMs, Jiang, Liu, Cho, and Oermann argue that Safe Harbor was built for structured records and leaves correlations between identity and clinical content in the text. In one experiment, with all other information removed from the notes, a model predicted a patient's neighborhood from the diagnosis alone. It's a position paper, not a settled result.

For an agent, this limits what a detector's answer means. An identifier detector tells you whether an identifier is present, not whether the text could identify someone, so a clean result on a discharge summary doesn't make it de-identified. De-identification is a determination about a data set, made under Safe Harbor or by an expert under 164.514(b)(1).

When do you detect, redact, or de-identify PHI in an agent?

Use detection that blocks on tool calls, redaction on text you still have to send, and de-identification when a data set needs to leave HIPAA's scope. Detection labels identifiers. Redaction replaces them so the text can continue. De-identification is a status that a data set reaches under 45 CFR 164.514(b), and no runtime filter grants it.

If a send_referral call to an outside fax service carries an MRN that your policy says can't leave, the call shouldn't happen, and the agent gets a refusal it can report or hand to a person. Redacting the argument instead changes what the tool does with no one deciding it. A lookup with a blanked ID returns the wrong record, and a letter with the name removed reaches a provider who can't match it to a chart.

Redaction fits text that has to reach its destination anyway. A model rewriting a discharge summary in plain language needs the clinical content, not the patient's name or MRN. HIPAA's minimum necessary standard in 45 CFR 164.502(b) points the same way, limiting most uses and disclosures of PHI to what the purpose requires. Redacted text still isn't de-identified text.

Arcjet's PHI detection doesn't redact or de-identify. For redaction in your own process before a model request, see how to redact sensitive data before sending it to OpenAI or Anthropic.

Where do you check for PHI in an AI agent?

Check at five points: the user's prompt, the arguments of each tool call, the results that tools return, the agent's writes to memory, and the logs your application keeps. Each is a place where PHI enters or leaves the agent's context, and a check at one point doesn't cover the others.

Each point calls for a different response:

  • The user prompt is where most PHI enters, such as a pasted clinical note. Check it before the model call.
  • Tool arguments are where PHI leaves. An MRN from three turns earlier can end up in a ticket, an email, or a call to a Model Context Protocol (MCP) server, which exposes third-party tools to the agent. Check before the tool runs, and block there.
  • Tool results bring in PHI from systems such as an EHR lookup. In your own agent, check a result before you return it to the model.
  • Memory writes outlast the session and can surface for a different user later. Treat each memory write as a tool call.
  • Logs often trace prompts and tool output in full, into an observability tool that a BAA may not cover. Don't log content you don't need.

For the wider set of controls, see how to stop AI agents leaking PHI.

Where does PHI detection run, and what does Arcjet keep?

The 20 core types can be detected in your process, and the matched text stays there. The five PHI types are detected only on Arcjet's servers, so detecting an MRN means sending the text to Arcjet. Arcjet removes a flagged value before storage and keeps its entity types and positions, but it stores unflagged text with the decision.

In your process, the SDK-local sensitiveInfo rule and the SDK-local detector run Rampart, an on-device ONNX model, against the 20 version 1 types. If those types are all you need, keep detection local, as described in keeping security inspection local.

The PHI types need the server-side detector, which reads a SERVER policy input, and a server input sends its raw value to Arcjet. Arcjet supports HIPAA compliance and offers a HIPAA-compliant environment. It enters into a BAA with Enterprise-level customers whose use of Arcjet involves PHI, and both parties must sign it before any PHI is processed through Arcjet. Using Arcjet for PHI without a signed BAA may leave your organization out of compliance. Arcjet's SOC 2 Type 2 report is in the Arcjet Trust Center.

Arcjet stores server policy inputs with Guard decisions for 90 days, and backups can hold data longer. A clinical note with no flagged identifier is stored as written. With zero data retention, for which you can check eligibility at support@arcjet.com, Arcjet keeps the decision structure, such as the conclusion and the rules that fired, but not prompts, tool arguments, or server input values. Some records still carry values: request decisions from protect() keep request metadata, such as the IP address, path, headers, and query, Guard custom rules keep their inputs, and session analysis keeps session data, including a screened tool-result excerpt, while a session is active and for up to 24 hours after. For more information, see Arcjet privacy.

How do you write a PHI detection policy for an agent tool call?

Pass the text to guard() as a server input, declare a sensitive information detector over it with the PHI types in its deny list, and deny in the policy's Rego when the detector finds one. Refuse the tool call when the decision is a denial or when evaluation failed open.

A policy is a set of rules that you publish from the Arcjet Console and select in code by label. A detector is a check that the policy declares over one of its inputs. Rego is the policy language of Open Policy Agent (OPA).

In the following example, a tool screens a note before it sends the note to another system. The policy declares two SERVER inputs, a string note and a boolean external, and a detector with the ID phi over note. The detector's deny list holds the five PHI types plus SSN, EMAIL, and PHONE_NUMBER, and it acts only on the types it names:

import { launchArcjet, policyInput } from "@arcjet/guard";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const decision = await arcjet.guard({
label: "notes.send", // selects the published policy with this label
actor: userId,
inputs: {
note: policyInput.server.string(note),
external: policyInput.server.boolean(isExternal),
},
});
if (decision.conclusion === "DENY" || decision.hasFailedOpen()) {
// refuse the tool call
}

When the client can't complete an evaluation, it returns ALLOW and hasFailedOpen() returns true, so checking both means an outage refuses the note.

The policy's Rego denies a note that carries a PHI identifier anywhere, and a note that carries any listed identifier when it leaves your organization:

package arcjet.guard
import rego.v1
# The identifier types that taxonomy version 2 adds for PHI.
phi_types := {
"MEDICAL_RECORD_NUMBER",
"HEALTH_PLAN_BENEFICIARY_NUMBER",
"LICENSE_PLATE_NUMBER",
"VEHICLE_IDENTIFICATION_NUMBER",
"DEVICE_IDENTIFIER",
}
# Deny a note that carries a PHI identifier.
deny contains "phi-in-note" if {
some entity in input.signals.sensitive_info.phi.entities
entity in phi_types
}
# Deny a note that carries any listed identifier when it leaves your
# organization.
deny contains "identifiers-to-external" if {
input.signals.sensitive_info.phi.detected
input.values.external
}

The detected field is true when the detector finds a listed type, and entities lists the types it found. To deny on every listed type with no Rego, declare a DETECTOR rule over the detector instead. For more information, see Detect protected health information.

Why does a published PHI policy keep its taxonomy version?

Each server-side detector stays on the taxonomy version that it was written against, so entity types that Arcjet adds later don't change the decisions of a policy you already published. The five PHI types are in version 2. A detector that you add uses the current version and covers them, and you can move an older detector forward.

The taxonomy is the list of entity types that Arcjet detects. Without the pin, a support agent's version 1 policy could start refusing a customer who describes a parking dispute once LICENSE_PLATE_NUMBER arrived. In the other direction, adding MEDICAL_RECORD_NUMBER to a version 1 detector's deny list has no effect until you move the detector forward, and saving the policy warns you about it. Run that move in dry run first. For more information, see sensitive information taxonomy versions.

How does PHI detection work for coding agents?

Add the PHI types to the sensitive information policy that screens what a developer types, and Arcjet can deny a prompt that carries an MRN. Arcjet can't withhold a tool result, such as a fixture file of real patient rows, because that hook runs after the tool has run. Session analysis can flag the session instead, and the calls that could send data out are then refused.

A hook is a request that a coding agent sends to an outside service at set points, such as when the developer submits a prompt. Claude Code, GitHub Copilot, Cursor, OpenAI Codex, and Muse Code call Arcjet from hooks they already fire, so one policy covers all five, installed through managed settings that a developer can't remove without administrator access. The coding-agent.sensitive-info starter declares a server-side detector over the prompt input that denies CREDIT_CARD_NUMBER and SSN. Add the PHI types to its deny list and set Execute on to Prompt only.

When the agent reads a fixture of patient rows, the content is already in its context. Session analysis screens tool results and flags the session with sensitive_data_exposure when a result contains an entity type that the session analysis policy's sensitive information detector denies. The coding-agent.session-analysis starter's detector denies only CREDIT_CARD_NUMBER and SSN, so add the PHI types to its deny list too. After the flag, that starter refuses web and MCP calls, which could send data out, while shell commands and file writes continue unless you add shell to that set. The agent can't clear the flag. Some analysis runs in the background, so the agent can make more calls before the flag lands, and nothing undoes what happened before it. The lasting fix for fixtures is synthetic data. For more information, see HIPAA for coding agents.

How do you roll out PHI detection without blocking real work?

Publish the policy in dry run, send it synthetic samples shaped like your real traffic, and review in the Arcjet Console what it would have denied. A dry-run rule records what it would deny without blocking anything. Set rules live after you've seen how the detector treats your formats.

Don't test with real patient records before a BAA is signed. Build fake samples from your production formats, such as an MRN with a label and one without, a CSV with an mrn header, a VIN in an auto-injury claim, and a device serial number in a remote-monitoring note. Add near misses, such as order numbers and loyalty numbers next to the word "member", to see what a live rule would break.

Then decide what a denial looks like to the person on the other end. An agent that says the note contains a medical record number lets a clinician fix it. Dates, ages over 89, and identifying clinical detail still need controls of their own. For how these checks fit a HIPAA program, see HIPAA compliance for AI agents.

Learn more: Detect protected health information · Sensitive information entity types · Coding agent policies · Arcjet privacy

Frequently asked questions

How do you detect PHI in LLM prompts?

Screen each prompt for the identifiers that tie health information to a person, such as names, medical record numbers, health plan member IDs, phone numbers, and Social Security numbers, before the model call. Arcjet's server-side sensitive information detector finds these identifiers so a policy can deny the prompt. Check tool arguments, tool results, memory writes, and logs as well, because PHI also enters and leaves an agent there.

How does PHI detection find a medical record number?

Medical record numbers and member IDs have no fixed format, so they look like any short code, such as an order number. Arcjet recognizes them from the surrounding text, such as an MRN or Member ID label next to the value. Identifiers with a fixed format, such as a VIN, an IMEI, or a MAC address, are recognized from their structure. Context-based detection can miss an unlabeled identifier and can flag a look-alike, so test against samples like your own traffic in dry run first.

Which of the 18 HIPAA Safe Harbor identifiers does Arcjet detect?

Arcjet has entity types for 14 of the 18 classes in 45 CFR 164.514(b)(2), three of them only in part: geographic subdivisions have no county, precinct, or geocode type, account numbers are covered for bank accounts only, and certificate and license numbers for driver's licenses only. Dates and ages over 89, biometric identifiers, full-face photographs, and any other unique identifying number, characteristic, or code are unsupported.

Does removing the 18 HIPAA identifiers de-identify clinical text?

Not on its own. Safe Harbor also requires that the organization has no actual knowledge that the remaining information could identify the person, and clinical detail such as a rare diagnosis or an occupation can do that. An identifier detector tells you whether an identifier is present, not whether the text is de-identified. De-identification is a determination about a data set under 45 CFR 164.514(b).

Should an AI agent redact PHI or block the tool call?

Block a tool call that carries an identifier your policy says can't leave, because redacting the arguments changes what the tool does without anyone deciding it. Redact text that has to reach its destination anyway, such as a summary sent to a model that needs the clinical content but not the name. Arcjet's PHI detection finds identifiers for a policy to block on; it doesn't redact or de-identify.

Does Arcjet sign a BAA for PHI detection?

Arcjet enters into a business associate agreement with Enterprise-level customers whose use of Arcjet involves PHI, and both parties must sign it before any PHI is processed through Arcjet. The PHI entity types are detected only by the server-side detector, so the text is sent to Arcjet. Arcjet removes a value its detector flags before storage and keeps only the entity types and positions.

AI runtime security in your code

Protect your AI agent workflows with Arcjet

Arcjet's server-side sensitive information detector finds medical record numbers, member IDs, and device and vehicle identifiers in the text your agent handles, so a policy can deny the prompt or tool call. Start it in dry run against samples shaped like your own traffic.