Wiki

Architecture, guardrail patterns, FHIR concepts, and how-tos for HealthClaw Guardrails.

Overview

HealthClaw Guardrails is a vendor-neutral guardrail proxy that sits between any AI agent and any FHIR health data server. It is a healthclaw.io open source project.

The core problem it solves: FHIR standardized how health data is structured. MCP standardized how AI connects to tools. Nobody standardized the guardrails in between — who can see what data, who can write what, what gets logged, and when a human must be involved. That's what HealthClaw Guardrails does.

Architecture

AI Agent ──▶ MCP Server ──▶ Guardrail Proxy ──▶ Any FHIR Server
(Claude,       (Node.js)       (Flask Python)       (HAPI, Epic,
 GPT-4, etc)                       ↓                Medplum, etc.)
                             PHI redaction
                             Audit trail
                             Step-up auth
                             Tenant isolation
                             Human-in-the-loop

Request Flow

  1. AI agent calls an MCP tool (e.g. fhir.read)
  2. MCP Server translates the tool call to an HTTP request to the Flask guardrail proxy
  3. Flask checks: tenant header present? Step-up token valid (for writes)? Human-confirmed (for clinical writes)?
  4. Resource fetched from local DB or upstream FHIR server
  5. PHI redacted from the response
  6. AuditEvent written (append-only)
  7. Medical disclaimer injected
  8. Redacted response + MCP summary returned to agent

Components

ComponentTechPurpose
Flask AppPython / FlaskFHIR REST facade at /r6/fhir/*, guardrail enforcement
MCP ServerNode.js / TypeScript12 MCP tools, Streamable HTTP + SSE transports
DatabaseSQLite (dev) / PostgreSQL (prod)Resource storage, audit trail, context envelopes
RedisOptionalPer-tenant rate limiting, session storage
Upstream FHIRAny FHIR R4/R5/R6 serverReal clinical data source (proxy mode)

Guardrail Stack

All guardrails are always active — they cannot be disabled per-request. They apply in local mode and upstream proxy mode identically.

PHI Redaction

Applied on every read path, including upstream FHIR server responses, before data reaches the AI agent.

FieldWhat happensExample
NameTruncated to initialsMaria Elena Rivera → M. E. Rivera
IdentifierMasked to last 4 charsMRN12345678 → ***5678
AddressStripped to city/state/country123 Main St, Boston, MA → Boston, MA
Birth dateTruncated to year1985-03-15 → 1985
TelecomFully removed617-555-0198 → [removed]
PhotoFully removed

Step-up Authorization

Reads require only X-Tenant-Id. All write operations additionally require an X-Step-Up-Token header.

  • Tokens are HMAC-SHA256 signed with STEP_UP_SECRET
  • Each token includes a 128-bit cryptographically random nonce
  • Tokens expire after 5 minutes
  • Tokens are tenant-bound — cross-tenant replay is rejected

Tokens are issued at GET /r6/fhir/internal/step-up-token (with tenant header).

Human-in-the-Loop

Clinical resource writes return HTTP 428 Precondition Required unless the request includes X-Human-Confirmed: true.

Clinical types: Observation, Condition, MedicationRequest, DiagnosticReport, AllergyIntolerance, Procedure, CarePlan, Immunization, NutritionIntake, DeviceAlert.

Current limitation: This is header-based, not cryptographic. Anyone with API access can set the header. It enforces intent in automated pipelines, but a determined attacker with API access could bypass it. A future version will use signed human-confirmation tokens.

Audit Trail

Every resource access writes an AuditEvent record. Records are append-only — SQLAlchemy event listeners raise RuntimeError on any UPDATE or DELETE.

Each AuditEvent records: event type, resource type + ID, tenant ID, agent ID, outcome (success/failure), timestamp, and optional detail.

Export via GET /r6/fhir/AuditEvent/$export in NDJSON or Bundle format.

Tenant Isolation

Every database query is scoped by tenant_id. Resources created by tenant A are invisible to tenant B. The X-Tenant-Id header is required on all non-public endpoints and validated against [a-zA-Z0-9_-]{1,64}.

FHIR Resources

US Core v9 R4 Stable

Standard FHIR R4 resources conforming to the US Core Implementation Guide v9. Widely deployed in US healthcare.

ResourceUS Core required fieldsCuratr evaluates
Conditioncode, subjectYes — ICD-10, SNOMED, deprecated code detection
AllergyIntoleranceclinicalStatus, verificationStatus, patientYes — RxNorm/SNOMED, status validation
Immunizationstatus, vaccineCode, patient, occurrence[x]Yes — CVX/SNOMED, occurrence date
MedicationRequeststatus, intent, medication[x], subjectYes — RxNorm code quality
Procedurestatus, code, subjectYes — SNOMED/CPT code quality
DiagnosticReportstatus, code, subjectYes — LOINC code quality
CarePlanstatus, intent, subjectNo — CRUD only
GoallifecycleStatus, description, subjectNo — CRUD only
Coveragestatus, beneficiary, payorNo — CRUD only
ServiceRequeststatus, intent, subjectNo — CRUD only
DocumentReferencestatus, subject, contentNo — CRUD only
Location, Organization, Practitioner, PractitionerRole, RelatedPerson, CareTeam, Specimen, FamilyMemberHistoryVariesNo — CRUD only

R6 Experimental Ballot3

FHIR R6 v6.0.0-ballot3 resources. These are experimental and may change before R6 final release.

ResourceWhat's new in R6
PermissionAccess control separate from Consent. $evaluate with human-readable reasoning.
SubscriptionTopicRestructured pub/sub. Storage + discovery — notifications not dispatched.
DeviceAlertISO/IEEE 11073 device alarms.
NutritionIntakeDietary consumption tracking.
DeviceAssociation, NutritionProduct, Requirements, ActorDefinitionCRUD only.

MCP Tools

The MCP server exposes 12 tools in two tiers. All read tools include an _mcp_summary field with reasoning, clinical context, and limitations to help the AI agent understand what it received and what it should tell the user.

Two-Phase Write Pattern

  1. Propose: Call fhir.propose_write. Validates the resource and returns a preview. Nothing is written.
  2. Review: Check requires_human_confirmation (true for clinical types) and proposal_status.
  3. Commit: Call fhir.commit_write with X-Step-Up-Token and X-Human-Confirmed: true for clinical resources.

Permission Evaluation

fhir.permission_evaluate evaluates stored R6 Permission resources against a subject + action + resource combination. Returns permit/deny with a human-readable reasoning string explaining which rule matched (or why default deny applied). This separates access control from consent records.

Curatr — Patient-Owned Data Quality

Curatr is the patient-facing skill within HealthClaw. It gives patients visibility into the quality of their own health records and the ability to correct them.

How it works

  1. Call curatr.evaluate on a resource
  2. Issues returned with severity (critical/warning/info/suggestion), plain language description, clinical impact, and suggested fix
  3. Present issues to the patient — explain what's wrong, why it matters, what can be done
  4. Patient approves specific fixes
  5. Call curatr.apply_fix — resource updated + linked Provenance resource created

Terminology services used

ServiceValidatesAuth needed
tx.fhir.org (HL7 public)SNOMED CT, LOINC, ICD-10None
NLM Clinical Tables APIICD-10-CM codes + descriptionsNone
RXNAV API (NLM)RxNorm drug codesNone
Local lookupDeprecated systems (ICD-9-CM, etc.)None

Provenance tracking

Every approved fix creates a FHIR Provenance resource recording: target resource, recorded timestamp, patient intent as free text, agent attribution (curatr), and exact field changes (before/after values).

Fasten Connect — Patient Health Record Ingestion

Fasten Connect is the ingestion layer for HealthClaw Guardrails. Patients authorize access to their EHR systems via the Fasten Stitch widget; records arrive as FHIR R4 NDJSON and flow through the full guardrail stack once ingested.

Two modes: Standard mode connects to individual EHR portals (Epic, Cerner, Athena). TEFCA IAS mode uses a single identity verification (CLEAR or ID.me) to pull longitudinal records across all QHINs — no per-provider logins required.

Required environment variables

VariableWhere to get itRequired
FASTEN_PUBLIC_KEYFasten Developer Portal — starts with public_test_ or public_live_Yes
FASTEN_PRIVATE_KEYSame portal — never expose client-sideYes
FASTEN_WEBHOOK_SECRETPortal → Delivery Logs → Signing secret (starts with whsec_)Recommended
FASTEN_CURATR_SCANSet true to auto-run Curatr after each importNo

Quick Start — OpenClaw

Step 1: Start the stack

export FASTEN_PUBLIC_KEY=public_test_XXX
export FASTEN_PRIVATE_KEY=private_test_XXX
export FASTEN_WEBHOOK_SECRET=whsec_XXX
export STEP_UP_SECRET=$(openssl rand -hex 32)
export FASTEN_CURATR_SCAN=true

docker-compose up -d --build

Step 2: Enable the skill in OpenClaw

Place the skill in your workspace skills folder or install from the project:

# Copy the skill to your OpenClaw workspace skills folder
cp -r skills/fasten-connect ~/.openclaw/skills/

# Or run from this project directory — OpenClaw picks up workspace skills automatically

OpenClaw reads FASTEN_PUBLIC_KEY from your environment (declared in metadata.openclaw.requires.env). The skill gates itself — it won't appear active until the key is set.

Step 3: Embed the Stitch widget in your web app

<!-- Add to any HTML page your patients use -->
<link rel="stylesheet" href="https://stitch.fastenhealth.com/v0.4/bundle.css">
<script src="https://stitch.fastenhealth.com/v0.4/bundle.js"></script>

<fasten-stitch-element public-id="public_test_XXX"></fasten-stitch-element>

<!-- TEFCA mode: single identity verification for all QHINs -->
<!-- <fasten-stitch-element public-id="public_test_XXX" tefca-mode="true"></fasten-stitch-element> -->

<script>
document.querySelector('fasten-stitch-element')
  .addEventListener('widget.complete', async (event) => {
    const { org_connection_id, endpoint_id, tefca_directory_id, platform_type } = event.detail;
    // Register the connection with HealthClaw
    await fetch('/fasten/connections', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Tenant-Id': 'YOUR_TENANT_ID' },
      body: JSON.stringify({ org_connection_id, endpoint_id, tefca_directory_id, platform_type }),
    });
  });
</script>

Step 4: Configure your webhook

In the Fasten Developer Portal, set your webhook delivery URL to:

https://your-domain.com/fasten/webhook

For local development, use ngrok or the Fasten Webhook Simulator.

Step 5: Use OpenClaw to work with ingested data

Once an import completes, the fhir-r6-guardrails and curatr skills expose all ingested data through MCP. In OpenClaw Messenger:

You: "Show me my recent conditions"
→ fhir.search(patient, Condition) — returns redacted conditions

You: "Check my diabetes diagnosis for quality issues"
→ curatr.evaluate(Condition, cond-001) — checks ICD-10, SNOMED, required fields

You: "Update the code to ICD-10-CM E11.9"
→ Patient approves → curatr.apply_fix() — writes fix + Provenance record

Check import status:

curl /fasten/jobs -H "X-Tenant-Id: your-tenant-id"
# Returns: [{"task_id": "...", "status": "complete", "ingested_resources": 847, ...}]
Production note: Fasten exports can be 30MB–3GB. Vercel's 60-second function timeout will cut off large downloads. Deploy to Railway (see railway.toml) or any persistent server for production use.

Quick Start — Claude Code / Claude Desktop

Step 1: Start the HealthClaw stack

export FASTEN_PUBLIC_KEY=public_test_XXX
export FASTEN_PRIVATE_KEY=private_test_XXX
export FASTEN_WEBHOOK_SECRET=whsec_XXX
export STEP_UP_SECRET=$(openssl rand -hex 32)

# Start Flask + MCP server
docker-compose up -d --build

# Or manually:
uv sync && python main.py &
cd services/agent-orchestrator && npm ci && npm start

Step 2: Point Claude at the MCP server

Add to ~/.claude/settings.json (Claude Code) or your Claude Desktop MCP config:

{
  "mcpServers": {
    "healthclaw-guardrails": {
      "url": "http://localhost:3001/mcp",
      "headers": {
        "X-Tenant-Id": "my-tenant"
      }
    }
  }
}

Step 3: Register a test connection

Use Fasten's test credentials to connect a synthetic patient, then register the org_connection_id:

# After patient authenticates via Stitch widget, register the connection:
curl -X POST http://localhost:5000/fasten/connections \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: my-tenant" \
  -d '{"org_connection_id": "org-conn-test-001", "platform_type": "epic"}'

# Trigger the export via Fasten API:
curl -X POST https://api.connect.fastenhealth.com/v1/bridge/fhir/ehi-export \
  -u "public_test_XXX:private_test_XXX" \
  -H "Content-Type: application/json" \
  -d '{"org_connection_id": "org-conn-test-001"}'

Step 4: Ask Claude to work with the data

Once the import webhook fires and ingestion completes, Claude has full access via MCP:

# In Claude Code (claude.ai/code) or Claude Desktop:

"Check the import status"
→ (uses curl or direct) GET /fasten/jobs

"Search for my conditions"
→ fhir.search with resourceType=Condition, patient filter

"Evaluate this condition for data quality issues"
→ curatr.evaluate on the Condition resource

"What guardrails apply to writing a new observation?"
→ fhir.propose_write to see validation + HITL requirements before committing

Step 5: Load the skill files for context

In Claude Code, the skill files provide authoritative reference without token overhead:

# The skills are in the workspace — Claude Code loads them automatically.
# Or reference them directly:
cat skills/fasten-connect/SKILL.md     # Fasten integration reference
cat skills/fhir-r6-guardrails/SKILL.md # MCP tool reference
cat skills/curatr/SKILL.md             # Curatr evaluation reference
TEFCA IAS (Claude platform): Enable TEFCA mode by setting tefca-mode="true" on the Stitch widget. For test mode, use synthetic patients from the Fasten test credentials page. Use tefca_directory_id (not endpoint_id) as the stable identifier when registering TEFCA connections.

Upstream FHIR Proxy

Set FHIR_UPSTREAM_URL to enable proxy mode. All guardrails remain active.

  • Reads: Fetched from upstream, redacted, audited, disclaimers added
  • Searches: All query parameters forwarded; results redacted per entry
  • Writes: Validated locally first, then forwarded with step-up auth enforcement
  • URL rewriting: Upstream URLs never appear in client responses
Proxy mode does not cache responses, does not translate between FHIR versions, and does not forward SMART-on-FHIR auth to upstream (uses upstream's native auth model).

Deployment

uv sync
STEP_UP_SECRET=your-secret python main.py

# With upstream FHIR server
FHIR_UPSTREAM_URL=https://hapi.fhir.org/baseR4 STEP_UP_SECRET=secret python main.py

# Docker Compose (Flask + MCP server + Redis)
docker-compose up -d --build

# MCP server only
cd services/agent-orchestrator && npm ci && npm start

Known Limitations

  • Local mode storage: SQLite JSON blobs — table scans for filtering, no indexed search fields
  • Structural validation only: No StructureDefinition conformance, cardinality checks, or terminology binding validation
  • SubscriptionTopic: Stored and discoverable, but notifications are not dispatched
  • Human-in-the-loop: Header-based, not cryptographic — enforces intent in pipelines, not against a determined attacker
  • No historical versioning: version_id increments but old versions are not retrievable
  • Context envelope consent: Always set to 'permit' (consent decision not evaluated)
  • De-identification: Applied at read time, not storage time
  • Upstream proxy: No response caching, no cross-version translation, no upstream auth forwarding