Architecture, guardrail patterns, FHIR concepts, and how-tos for HealthClaw Guardrails.
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.
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
fhir.read)| Component | Tech | Purpose |
|---|---|---|
| Flask App | Python / Flask | FHIR REST facade at /r6/fhir/*, guardrail enforcement |
| MCP Server | Node.js / TypeScript | 12 MCP tools, Streamable HTTP + SSE transports |
| Database | SQLite (dev) / PostgreSQL (prod) | Resource storage, audit trail, context envelopes |
| Redis | Optional | Per-tenant rate limiting, session storage |
| Upstream FHIR | Any FHIR R4/R5/R6 server | Real clinical data source (proxy mode) |
All guardrails are always active — they cannot be disabled per-request. They apply in local mode and upstream proxy mode identically.
Applied on every read path, including upstream FHIR server responses, before data reaches the AI agent.
| Field | What happens | Example |
|---|---|---|
| Name | Truncated to initials | Maria Elena Rivera → M. E. Rivera |
| Identifier | Masked to last 4 chars | MRN12345678 → ***5678 |
| Address | Stripped to city/state/country | 123 Main St, Boston, MA → Boston, MA |
| Birth date | Truncated to year | 1985-03-15 → 1985 |
| Telecom | Fully removed | 617-555-0198 → [removed] |
| Photo | Fully removed | — |
Reads require only X-Tenant-Id. All write operations additionally require an X-Step-Up-Token header.
STEP_UP_SECRETTokens are issued at GET /r6/fhir/internal/step-up-token (with tenant header).
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.
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.
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}.
Standard FHIR R4 resources conforming to the US Core Implementation Guide v9. Widely deployed in US healthcare.
| Resource | US Core required fields | Curatr evaluates |
|---|---|---|
| Condition | code, subject | Yes — ICD-10, SNOMED, deprecated code detection |
| AllergyIntolerance | clinicalStatus, verificationStatus, patient | Yes — RxNorm/SNOMED, status validation |
| Immunization | status, vaccineCode, patient, occurrence[x] | Yes — CVX/SNOMED, occurrence date |
| MedicationRequest | status, intent, medication[x], subject | Yes — RxNorm code quality |
| Procedure | status, code, subject | Yes — SNOMED/CPT code quality |
| DiagnosticReport | status, code, subject | Yes — LOINC code quality |
| CarePlan | status, intent, subject | No — CRUD only |
| Goal | lifecycleStatus, description, subject | No — CRUD only |
| Coverage | status, beneficiary, payor | No — CRUD only |
| ServiceRequest | status, intent, subject | No — CRUD only |
| DocumentReference | status, subject, content | No — CRUD only |
| Location, Organization, Practitioner, PractitionerRole, RelatedPerson, CareTeam, Specimen, FamilyMemberHistory | Varies | No — CRUD only |
FHIR R6 v6.0.0-ballot3 resources. These are experimental and may change before R6 final release.
| Resource | What's new in R6 |
|---|---|
| Permission | Access control separate from Consent. $evaluate with human-readable reasoning. |
| SubscriptionTopic | Restructured pub/sub. Storage + discovery — notifications not dispatched. |
| DeviceAlert | ISO/IEEE 11073 device alarms. |
| NutritionIntake | Dietary consumption tracking. |
| DeviceAssociation, NutritionProduct, Requirements, ActorDefinition | CRUD only. |
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.
fhir.propose_write. Validates the resource and returns a preview. Nothing is written.requires_human_confirmation (true for clinical types) and proposal_status.fhir.commit_write with X-Step-Up-Token and X-Human-Confirmed: true for clinical resources.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 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.
curatr.evaluate on a resourcecuratr.apply_fix — resource updated + linked Provenance resource created| Service | Validates | Auth needed |
|---|---|---|
| tx.fhir.org (HL7 public) | SNOMED CT, LOINC, ICD-10 | None |
| NLM Clinical Tables API | ICD-10-CM codes + descriptions | None |
| RXNAV API (NLM) | RxNorm drug codes | None |
| Local lookup | Deprecated systems (ICD-9-CM, etc.) | None |
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 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.
| Variable | Where to get it | Required |
|---|---|---|
FASTEN_PUBLIC_KEY | Fasten Developer Portal — starts with public_test_ or public_live_ | Yes |
FASTEN_PRIVATE_KEY | Same portal — never expose client-side | Yes |
FASTEN_WEBHOOK_SECRET | Portal → Delivery Logs → Signing secret (starts with whsec_) | Recommended |
FASTEN_CURATR_SCAN | Set true to auto-run Curatr after each import | No |
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, ...}]
railway.toml) or any persistent server for production use.
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-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.
Set FHIR_UPSTREAM_URL to enable proxy mode. All guardrails remain active.
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