---
{
  "title": "We standardized how to get health data. We never standardized what an agent may do with it.",
  "description": "FHIR settled who may read a patient's record. It never settled what an AI agent may do with that record afterward. Here is the gap, and a runnable Aidbox example that closes part of it.",
  "date": "2026-08-24",
  "author": "Eugene Vestel",
  "reading-time": "12 min read",
  "tags": ["AI / Agents", "Compliance", "FHIR Standard", "Aidbox"],
  "tldr": "FHIR's authorization model answers access questions: may this client read this resource? It has no vocabulary for use questions — may the reader keep a copy, send it to a model provider, train on it, act on an inference. Prompts are not enforcement, so the controls have to run on the server: redact on read, audit every call, step up on writes, and require out-of-band approval for anything irreversible. HealthClaw Guardrails is an MIT-licensed proxy that implements those four controls, and this post walks through running it in front of Aidbox.",
  "utm-campaign": "ai",
  "utm-content": "agent-guardrails"
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---

A woman I know has three chronic conditions, four prescribers, and records in six systems. Before every new appointment she does the same work: log into three portals, screenshot her medication list, retype her history onto a clipboard form, and hope the front desk enters it correctly. She has done this for eleven years. She is not a bad patient. She is doing unpaid integration labor because nobody else in the system is positioned to do it.

That is the bottleneck. Not diagnosis, not access to data in the legal sense. The patient is the only party who has the whole picture and the only party with no tooling to act on it.

An AI agent is the first thing that plausibly closes that gap. It can read across six systems, reconcile a medication list, fill the form, and chase the referral. This is not speculative. Connect an MCP client to a FHIR server, hand it a base URL and a token, and the demo works on the first try.

Which is exactly the problem. The demo works, and nothing in the stack tells the patient what that agent may do with what it just read.

## What FHIR was designed to answer, and what it wasn't

FHIR's authorization model was built for a world where the client was an application and a human sat behind it. That world started around 2011, hardened through Argonaut and SMART on FHIR, and became law through the 21st Century Cures API rules. It solved a real problem, and it solved it well. A patient can now get their data.

Look at what the scopes actually say. `patient/Observation.rs` means this client may read and search Observations for this patient. That is an access question, and FHIR answers it precisely.

Now ask the questions an agent raises:

- May this reader keep a copy after the session ends?
- May it send the payload to a model provider in another jurisdiction?
- May the provider retain it in a prompt cache?
- May it be used to train a model that someone else sells?
- May the reader infer a diagnosis and act on the inference somewhere the patient never sees?

FHIR does not answer any of these, because none of them are access questions. They are use questions, and use was somebody else's problem when the spec was written. `Consent` gets closest: it expresses permission to disclose, with purpose-of-use and actor provisions. It still describes a permission granted at the boundary. Nothing in the resource travels with the payload, and nothing at the recipient enforces it. R6 introduces `Permission` with an `$evaluate` operation, which is a genuine step and is still in ballot.

So the honest statement of the gap: the standard governs who may read. It does not govern what the reader may do afterward, and it has no vocabulary for a reader that is a model rather than a person.

This is not a criticism of FHIR. Grahame and everyone who built it were solving 2014's problem, and the problem was real. It is a statement about what 2026 needs that does not exist yet.

## Nobody is building the patient's side of this

There is real work happening here, and it deserves naming. The CARIN Alliance has a code of conduct for consumer-directed exchange. Josh Mandel's SMART Health Links gave patients a genuine mechanism to share a record on their own terms. The Consent IG exists. HL7 working groups are engaged.

But look at who the AI governance tooling is being built by and for. Vendors are building agent safety into their own products, scoped to their own liability. Health systems are writing AI policies that govern the system's agents inside the system's boundary. Both are rational. Neither produces anything the patient controls.

The asymmetry is the point. When a health system deploys an agent, the system decides the redaction policy, holds the audit trail, and sets the approval rules. When a patient uses an agent on their own records, the patient decides none of those things. They accept whatever the app vendor chose, usually without seeing it, and the app vendor has no incentive to choose conservatively.

We are about to hand patients the most capable tool they have ever had for navigating this system, with no governance layer they own. That is worth being uncomfortable about.

## What the copy is actually worth

Here is why this is not an abstract concern. When a fully identified FHIR resource leaves the covered entity and lands in a general-purpose AI vendor, HIPAA usually stops applying. There is no business associate agreement with a consumer chatbot. What governs the data there is FTC enforcement and a patchwork of state law, which is a weaker regime for the individual.

Four things can happen to that copy.

**It gets sold.** Health data brokerage is an established market. Buyers include advertisers, pharmaceutical marketers, and clinical trial recruitment vendors who pay for condition-level cohorts. The FTC's 2023 actions against GoodRx and BetterHelp both involved health data reaching advertising platforms from companies that had told users it would not. No hospital was involved in either case.

**It reaches an underwriter.** The ACA bars health insurers from underwriting on pre-existing conditions. It does not bar life, disability, or long-term care insurers, who do underwrite on health history. Employer exposure runs through wellness programs and self-funded plan administration, where the separation between plan data and employer data is thinner than most employees assume. GINA and the ADA constrain some of this. Neither was drafted with conditions inferred from a chat transcript in mind.

**It puts you in a cohort.** Once a third party can infer your diagnoses, you can be placed in an outreach program you never opted into, or shown a narrower set of provider options than exists. Whether steering happens at scale today is arguable. The incentive is not, and the patient has no visibility either way.

**It trains a model.** Depending on the vendor's terms, inputs may become training data, and consumer tiers differ sharply from enterprise tiers on exactly this point. This is the only item on the list that cannot be undone. A broker's copy can be deleted. Weights cannot be un-trained.

None of these require anyone to act in bad faith. They are the default behavior of a system where the patient granted read access and nothing downstream is constrained.

## Instructions are not enforcement

The common response is to write better instructions. Put the rules in the system prompt. Tell the agent to redact identifiers, log its actions, and ask before writing.

A prompt is a request, not a control. The model decides whether to honor it, and anyone who can put text in front of the model gets a vote. Clinical notes, scanned documents, and portal messages are all text an attacker can influence. A control the agent can talk its way past is not a control.

Enforcement has to run where the agent cannot reach it. In practice that means the server, and it means four things.

**Redact on read.** Strip identifier-class fields before the resource reaches the model. Names to initials, identifiers masked, addresses removed, birth dates truncated to year. The agent gets clinical content and not identity. This is a compensating control, not a legal de-identification determination, and it should be described that way. It does not make a record with a rare diagnosis and an unusual date sequence unlinkable. What it changes is the default, and the default is currently "hand over everything."

**Audit everything.** Every read and write emits a durable record naming the tenant, the agent, the resource, and the time. A request log is not this. When a compliance officer asks which agent, acting for which patient, read which resources, an access log carrying a shared service-account identity cannot answer. Two design rules matter: the trail is append-only, and the audit detail is PHI-free, so the record you hand to a reviewer is safe to hand over.

**Step up on writes.** At the protocol level, a token that can `GET /Observation` usually can `POST /Observation`. The transport does not distinguish "summarize my labs" from "record a blood pressure of 190/120." Requiring a separate short-lived credential for writes does not stop a determined attacker by itself. It makes writes a distinct, auditable event class instead of a side effect of a chatty session.

**Require out-of-band approval for anything irreversible.** For a clinical write, block until a human confirms. For a real-world action such as a call, a text, or a submitted form, the bar is higher: commit should only submit the action, with execution gated behind an approval the agent's own toolchain cannot produce. If the agent can supply the artifact that represents human consent, there is no human in the loop.

The obvious objection: SMART scopes and `Consent` already do some of this. Partly true, and it is where the pattern should eventually live. Scopes constrain what a client may request. What they cannot do is constrain the shape of the response, produce an agent-attributed audit record, or hold a write until a person taps approve. Those are runtime behaviors, and today no server does them by default.

## One implementation, built in the open

HealthClaw Guardrails is an MIT-licensed proxy between any AI agent and any FHIR server. It exposes an MCP server with 29 tools and a REST facade, and enforces the four controls above plus tenant isolation. The design rule is that no safety property depends on client behavior.

```mermaid
flowchart LR
    A[AI Agent] --> B[MCP Server]
    B --> C[Guardrail Proxy]
    C --> D["Any FHIR Server<br/>(Aidbox, HAPI, Epic, ...)"]
    C -.- E["PHI redaction<br/>Audit trail<br/>Step-up auth<br/>Human-in-the-loop<br/>Tenant isolation"]
```

Two details are where this pattern usually leaks.

**The write path.** `fhir_propose_write` validates and previews without committing. `fhir_commit_write` requires a step-up token and returns HTTP 428 until a human confirms. For real-world actions, `action_commit` returns `202 awaiting_confirmation` and does nothing else; execution consumes a single-use credential through a separate approval path, claimed atomically so it cannot be replayed. An earlier version gated this with an `X-Human-Confirmed` request header. We removed it from the action rail, because a header is spoofable by the caller that sets it. That header still gates clinical FHIR writes today, and we document it as a compensating control rather than proof a human acted. Being precise about which guarantees are cryptographic and which are conventions is most of the value here.

**Upstream URL rewriting.** Responses are rewritten so the backing server's base URL never reaches the client. An agent that learns the real endpoint will try to route around the proxy.

## Run it in front of Aidbox

The full example, including `docker-compose.yaml`, seed data, and a scripted walkthrough, lives at [aidbox-integrations/healthclaw-guardrails](https://github.com/Aidbox/examples/tree/main/aidbox-integrations/healthclaw-guardrails) in the Aidbox examples repo.

Get a free Aidbox license at [aidbox.app](https://aidbox.app), then:

```bash
git clone https://github.com/Aidbox/examples
cd examples/aidbox-integrations/healthclaw-guardrails
cp .env.example .env          # paste AIDBOX_LICENSE, set STEP_UP_SECRET
docker compose up -d
./scripts/seed-aidbox.sh      # one Patient, three Observations, one Condition
```

Three services come up: Aidbox on 8080 as the system of record, the guardrail proxy on 5000, and the MCP endpoint on 3001 that the agent connects to. Aidbox is configured with an `AccessPolicy` scoping the guardrail's client to the FHIR endpoint, and the proxy is pointed at it:

```yaml
healthclaw:
  environment:
    FHIR_UPSTREAM_URL: http://aidbox:8080/fhir
    STEP_UP_SECRET: ${STEP_UP_SECRET}
    READ_AUTH_ENABLED: "true"
```

Nothing about the Aidbox side is unusual. That is deliberate. The guardrail layer is additive, and the FHIR server underneath keeps behaving like a FHIR server.

### The same read, with and without governance

Direct to Aidbox, the record is fully identified, as it should be:

```bash
curl -u "$AIDBOX_CLIENT:$AIDBOX_SECRET" \
  http://localhost:8080/fhir/Patient/pt-demo
```

```json
{ "resourceType": "Patient", "id": "pt-demo",
  "name": [{"given": ["Maria"], "family": "Alvarez"}],
  "identifier": [{"system": "urn:mrn", "value": "MRN-88214"}],
  "birthDate": "1974-03-11",
  "address": [{"line": ["221 Baker St"], "city": "Pittsburgh"}] }
```

Through the proxy, same resource, same Aidbox:

```bash
curl -H "X-Tenant-ID: demo" \
  http://localhost:5000/r6/fhir/Patient/pt-demo
```

```json
{ "resourceType": "Patient", "id": "pt-demo",
  "name": [{"given": ["M."], "family": "A."}],
  "identifier": [{"system": "urn:mrn", "value": "***masked***"}],
  "birthDate": "1974",
  "meta": {"tag": [{"code": "redacted"}]} }
```

Aidbox still holds the complete record. Redaction is a property of the path the agent uses, not a modification of the data.

### The read left a record

```bash
curl -H "X-Tenant-ID: demo" "http://localhost:5000/r6/fhir/AuditEvent?_count=1"
```

Returns an `AuditEvent` naming the tenant, the agent, `Patient/pt-demo`, and the timestamp, with no PHI in the detail. `$export` emits the trail as NDJSON for a SIEM.

### A write, blocked twice

Ask the agent to record a blood pressure. First attempt, no step-up token, returns 401. Mint a token and retry, and it returns 428 pending human confirmation. Only after confirmation does the Observation reach Aidbox. Verify it landed by querying Aidbox directly, going around the proxy:

```bash
curl -u "$AIDBOX_CLIENT:$AIDBOX_SECRET" \
  "http://localhost:8080/fhir/Observation?subject=Patient/pt-demo&code=85354-9"
```

The resource is there, and the audit trail records who proposed it, who approved it, and when. That sequence is the whole argument. The agent did useful work. It could not finish alone.

### Grade the deployment

```bash
curl "http://localhost:5000/r6/fhir/\$conformance?format=text"
```

```text
HealthClaw Guardrail Conformance — http://localhost:5000 [tenant=demo]
  Grade: A   (7/7 properties)

  [PASS] PHI Redaction            [PASS] Human-in-the-Loop
  [PASS] Immutable Audit Trail    [PASS] Tenant Isolation
  [PASS] Step-Up Authorization    [PASS] Medical Disclaimers
  [PASS] Error Fidelity
```

Error fidelity is the least obvious property: unknown search parameters and unsupported modifiers must be rejected or surfaced, never silently dropped. A filter that disappears quietly widens a query, and a widened query on a patient record is a disclosure. The same harness runs in CI as a merge gate, so a regression shows up as a grade change instead of an incident. A safety claim you can run is worth more than one you can read.

## What this buys the patient

Back to the clipboard. In CareAgents, the consumer app built on this layer, the request is "I'm seeing a new doctor next week, fill out my intake form from my records." The agent populates the form using SDC `$populate`, and then stops.

Each medication and each allergy requires individual patient confirmation. "No known allergies" requires an explicit attestation and is never inferred from an empty list, because an empty list and a real negative are different clinical statements. The server re-derives the item list at submit time, so a crafted request cannot skip a row. Approval produces a provenance-stamped PDF behind a signed, expiring link.

Eleven years of retyping becomes a review-and-approve. The agent does the labor. The patient keeps every decision. The server makes that division non-negotiable rather than a promise in a privacy policy.

## Why this layer has to be open

**A safety property you cannot inspect is a marketing claim.** Every vendor says their agent is safe with PHI. Open source turns that into something a hospital security team can read, run, and attack. That is a different kind of assurance and it is the only kind that survives contact with a security review.

**Health IT standards get won by open reference implementations.** FHIR spread because HAPI, public test servers, Synthea, and connectathons made adoption cheap and faking it hard. Agent governance will standardize the same way or not at all.

**The threat model is larger than any one team.** Prompt injection through clinical documents, tenant-isolation bugs, silently dropped search parameters. These get found by many adversarial readers or they do not get found. We publish our own: an audit-write failure that rolled back the caller's transaction while returning success, and a sign-in input that truncated 8-digit codes to 6. In a closed product those are quiet patches. Here they became regression tests.

**A layer meant to outlive every model vendor should not belong to one.** Frontier APIs and open-weight local models are increasingly swappable behind a single adapter. What persists across model generations is the governance layer. If it belongs to a vendor, the patient's guarantees expire when that vendor's business model changes.

## What the community should do next

Three things, and none of them are a product.

**Write the missing profile.** We need a way to express, in FHIR, what a reader may do with a resource after receiving it. Not just permission to disclose, but retention, redisclosure, inference, and training. R6 `Permission` is the right place to start the conversation. Bring it to the working groups.

**Agree on a conformance contract for agent access.** Redact, audit, step up, human-approve, isolate tenants, preserve error fidelity. Argue with that list. Replace it with a better one. But settle on something a deployment can be graded against, so "our agent is safe" stops being an unfalsifiable sentence.

**Make the patient the one holding the policy.** Right now the redaction rule, the audit trail, and the approval gate all belong to whoever wrote the app. That is backwards for consumer-directed exchange, and fixing it is a design problem the community has not seriously taken up.

Health data should be easy to get. That fight is largely won. The next one is making it safe to act on, with the patient holding the controls rather than reading about them afterward.

HealthClaw Guardrails is MIT-licensed and small enough to read in an afternoon: a Python FHIR facade, a TypeScript MCP server, roughly 1,170 Python and 112 Node tests, and the conformance harness gating CI. It is a reference implementation and an argument, not a product. If you run Aidbox, clone the example and try to break it. The most useful thing you can send us is a payload that gets past a control it should not.

Example: [github.com/Aidbox/examples](https://github.com/Aidbox/examples/tree/main/aidbox-integrations/healthclaw-guardrails) · Repo: [github.com/aks129/HealthClawGuardrails](https://github.com/aks129/HealthClawGuardrails) · Live conformance: [app.healthclaw.io/r6/fhir/$conformance](https://app.healthclaw.io/r6/fhir/$conformance) · Consumer app: [careagents.cloud](https://careagents.cloud)

*Eugene Vestel writes at FHIR IQ and hosts the* Out of the FHIR *podcast. Health Samurai builds Aidbox, a FHIR platform for healthcare teams.*
