# HIPAA y LLM: la fuga de datos de pacientes como error de tipo

> En un pipeline de LLM, los datos de salud protegidos cruzan fronteras que ningún tipo ve. Una demo con salida real del compilador: quitas el control que los cubre y el programa deja de compilar. Y lo que eso demuestra, y lo que no.

- Fuente canónica: https://www.ricardovelit.com/blog/p/hipaa-llm-fuga-de-phi-error-de-tipo
- Nivel: intermedio · Lectura: 8 min · Publicado: 2026-09-23
- Etiquetas: hipaa, cumplimiento, axon, agentes, tipos
- Relacionados: [sistemas-multiagente](https://www.ricardovelit.com/blog/p/sistemas-multiagente.md), [json-es-la-caverna-de-platon](https://www.ricardovelit.com/blog/p/json-es-la-caverna-de-platon.md), [patrones-de-diseno-de-agentes-de-ia](https://www.ricardovelit.com/blog/p/patrones-de-diseno-de-agentes-de-ia.md)
- Contiene componentes interactivos que solo funcionan en la versión web.

---
## El fallo que no hace ruido

Una clínica usa un modelo de lenguaje para resumir notas clínicas para el médico de guardia.
El endpoint recibe la nota, la pasa por una capa que detecta y bloquea datos
identificables, llama al modelo y devuelve el resumen.

Un día alguien refactoriza el endpoint. Mueve la llamada al modelo a una función nueva, más
limpia, y en el camino la capa de protección se queda fuera. No falla ningún test, porque
los tests comprueban que el resumen sale bien, y sale bien. El linter no dice nada. La
revisión de código ve un diff que mejora la legibilidad. El cambio llega a producción, y
desde ese día las notas con nombres, fechas de nacimiento y números de historia clínica
viajan sin filtro hacia el proveedor del modelo y hacia los logs.

Nadie se entera hasta la auditoría. O hasta la brecha.

Ese es el problema de fondo del cumplimiento normativo en sistemas con IA: vive en un
documento de políticas, en una revisión y en un filtro que alguien tiene que acordarse de
conectar. Nada de eso forma parte del programa. El programa sin el filtro sigue siendo un
programa perfectamente válido.

## Qué pide HIPAA, en lo que toca a un LLM

HIPAA es la ley estadounidense que regula la información de salud. Lo que protege es la
**PHI** (*Protected Health Information*): información de salud unida a algo que identifica
a una persona. La vía más usada para anonimizar, llamada *Safe Harbor*, enumera 18
identificadores que hay que quitar: nombres, fechas, teléfonos, correos, números de la
seguridad social, números de historia clínica, entre otros.

Para un pipeline de LLM, eso se traduce en preguntas muy concretas:

- **¿Quién recibe la PHI?** Si la nota llega al proveedor del modelo, ese proveedor es un
  *business associate* y necesitas un contrato con él (un BAA).
- **¿Se envía solo lo necesario?** El principio de *mínimo necesario* pide no mandar más
  PHI de la que la tarea requiere.
- **¿Queda registro de cada acceso?** Los controles de auditoría de la regla de seguridad
  piden poder reconstruir quién vio qué y cuándo.

Todas tienen la misma forma: **cada lugar por donde la PHI cruza una frontera** —el prompt,
el proveedor, los logs, la respuesta hacia otro sistema— es un punto que tiene que estar
controlado.

## Por qué un lenguaje normal no puede verlo

En Python, TypeScript o Go, el texto de la nota clínica es un `string`. El mismo tipo que el
nombre de un botón. Nada en el tipo dice "esto es PHI", así que nada en el compilador puede
exigir que pase por un control antes de salir.

```python
def resumir(nota: NotaClinica) -> str:
    texto = redactar_phi(nota.texto)   # la protección es una llamada
    return llm(texto)                  # que se puede borrar sin romper nada
```

Borra la primera línea y cambia `texto` por `nota.texto`. El programa sigue compilando,
los tipos siguen cuadrando y los tests siguen en verde. Es la misma sombra de la que hablo
en [JSON es la caverna de Platón](/p/json-es-la-caverna-de-platon): el formato transporta
la forma del dato, no lo que el dato es.

## El mismo pipeline en AXON

Una advertencia antes de seguir, porque cambia cómo debes leer lo que viene: AXON lo
construyo yo. Es un lenguaje de programación compilado cuyo destino es un modelo en lugar
de un procesador —*AXON programming language* en la documentación, `axon-lang` en la
terminal—, y una de sus ideas centrales es justo esta: que la clase regulatoria de un dato
forme parte de su tipo. La documentación la llama
[cumplimiento en compilación](https://www.ricardovelit.com/axon-docs/es/concepts/compile-time-compliance).

Este es el pipeline de la clínica. Todo lo que sigue lo ejecuté con el compilador de AXON, y
las salidas están copiadas de la terminal; solo he partido las líneas largas para que se
lean.

```text
type ClinicalNote compliance [HIPAA] { patient_id: String, text: String }
type NoteSummary compliance [HIPAA] { patient_id: String, summary: String }

shield PHIShield {
  scan: [pii_leak]
  on_breach: halt
  severity: critical
  compliance: [HIPAA]
}

flow Summarize(patient_id: String, text: String) -> NoteSummary {
  step Summary {
    ask: "Summarize the clinical note for the attending physician."
    output: NoteSummary
  }
  return Summary.output
}

axonendpoint SummarizeNote {
  method: POST
  path: "/notes/summary"
  body: ClinicalNote
  execute: Summarize
  output: FlowEnvelope<NoteSummary>
  shield: PHIShield
  backend: anthropic
}
```

Cuatro piezas:

- Dos **tipos** que llevan la clase `HIPAA`. No es un comentario: viaja con el valor por
  todo el programa.
- Un **shield**, el control que actúa en tiempo de ejecución: busca fugas de datos
  identificables y, si encuentra una, detiene la ejecución. Declara qué clases cubre.
- Un **flow**, el trabajo cognitivo: el paso que le pide al modelo el resumen.
- Un **axonendpoint**, la frontera: recibe la nota por HTTP, ejecuta el flujo y devuelve
  el resultado. Declara qué shield la protege.

```text
$ axon check clinica.axon
✓ clinica.axon  108 tokens · 5 declarations · 0 errors
```

Compila. El endpoint transporta PHI y el shield que declara cubre `HIPAA`.

## Ahora quita el shield

Es el refactor de la historia del principio: borrar la línea `shield: PHIShield` del
endpoint.

```text
$ axon check sin_shield.axon
X sin_shield.axon  2 error(s)
  error [line 19]: axon-T890 axonendpoint 'SummarizeNote' declares no
  authorization coverage (no `requires:`, no `shield:`, no `compliance:`) and is
  not marked `public: true`. Every endpoint is a trust boundary — doctrine
  `every_boundary_is_guarded`: [...]
  error [line 19]: axon-T957 axonendpoint 'SummarizeNote' carries regulated data
  (kappa = {HIPAA}) across a trust boundary but declares no `shield:`. Regulated
  boundaries require a shield whose `compliance:` covers the type's kappa — ESK
  Fase 6.1 coverage rule. Declare `shield: <Name>` on the endpoint, where that
  shield lists at least [HIPAA] in its `compliance:`. Declaring the classes on
  the endpoint's own `compliance:` does NOT cover them: that list is a label, the
  shield is the control that acts on a breach.
```

`axon check` sale con código `1`, igual que ante un error de sintaxis, y no se construye
nada. El refactor que en Python llegaba a producción sin que nadie lo notara aquí no pasa
de la máquina de quien lo escribió.

El compilador da dos errores. El primero es general: todo endpoint es una frontera de
confianza y tiene que declarar qué lo protege, o decir explícitamente que es público. El
segundo es el de HIPAA: esta frontera transporta datos de clase `HIPAA` (la `kappa` del
mensaje es el conjunto de clases regulatorias) y no hay ningún shield que los cubra.

## Tres formas de intentar engañarlo

Un control que se puede esquivar con una línea no es un control. Así que probé las tres
trampas más obvias.

### 1. Poner la etiqueta en el endpoint

¿Y si en lugar del shield declaro `compliance: [HIPAA]` en el propio endpoint?

```text
$ axon check etiqueta.axon
X etiqueta.axon  1 error(s)
  error [line 19]: axon-T957 axonendpoint 'SummarizeNote' carries regulated data
  (kappa = {HIPAA}) across a trust boundary but declares no `shield:`. [...]
  Declaring the classes on the endpoint's own `compliance:` does NOT cover them:
  that list is a label, the shield is the control that acts on a breach.
```

Desaparece el primer error, porque el endpoint ya declara algo, pero no el de HIPAA. Es la
decisión más importante del diseño: **una etiqueta no es un control**. Si el compilador
aceptara la etiqueta como cobertura, un programa podría certificarse a sí mismo.

### 2. Un shield que cubre solo una parte

Ahora la nota también lleva datos de pacientes europeos, así que su tipo pasa a ser
`compliance [HIPAA, GDPR]`. Pero el shield solo declara `[GDPR]`.

```text
$ axon check parcial.axon
X parcial.axon  1 error(s)
  error [line 19]: axon-T957 axonendpoint 'SummarizeNote' declares `shield:
  PHIShield`, but that shield does not cover kappa = {HIPAA} carried across the
  boundary — ESK Fase 6.1 coverage rule. Add [HIPAA] to shield 'PHIShield's
  `compliance:` list, or name a shield that already covers them.
```

No basta con que haya un shield. La regla es una **diferencia de conjuntos**: las clases
que cruzan la frontera, menos las que cubre el shield, tienen que dar el conjunto vacío. El
diagnóstico nombra exactamente lo que falta.

### 3. Una errata

¿Y si escribo `HIPPA` en el shield? En un sistema de etiquetas libres, una errata simétrica
en el tipo y en el shield haría cuadrar la comparación sin proteger nada.

```text
$ axon check errata.axon
X errata.axon  2 error(s)
  error [line 19]: axon-T957 axonendpoint 'SummarizeNote' declares `shield:
  PHIShield`, but that shield does not cover kappa = {HIPAA} [...]
  error [line 4]: axon-T1214 shield 'PHIShield' declares `HIPPA`, which is not a
  regulatory class. Did you mean `HIPAA`? The canonical registry is ["HIPAA",
  "PCI_DSS", "GDPR", "SOX", "FINRA", "ISO27001", "SOC2", "FISMA", "GxP", "CCPA",
  "NIST_800_53", "NOM151", "LFPDPPP", "LGPD", "LEY1581"] (case-sensitive). A
  compliance label is an ASSERTION a regulated reader trusts — an unrecognised
  one is not a weaker claim, it is a claim about nothing that still satisfies
  every coverage law that compares these strings.
```

El vocabulario es cerrado: quince clases, y ninguna más. Eso es lo que hace que la regla de
cobertura sea sólida y no un juego de cadenas.

## Lo que el compilador no comprueba

Aquí conviene ser tan preciso como en lo anterior, porque es donde un artículo así suele
mentir por omisión.

Cambié el shield para que en lugar de buscar datos identificables buscara solo intentos de
manipulación del prompt, sin tocar nada más:

```text
shield PHIShield {
  scan: [prompt_injection]
  on_breach: halt
  severity: critical
  compliance: [HIPAA]
}
```

```text
$ axon check otro_scan.axon
✓ otro_scan.axon  108 tokens · 5 declarations · 0 errors
```

**Compila.** El shield declara que cubre `HIPAA`, así que la regla de cobertura se cumple,
aunque lo que escanea no tiene nada que ver con proteger datos de pacientes.

Eso marca el límite exacto de lo que se demuestra. El compilador garantiza que **en cada
frontera por la que cruza PHI hay un control declarado para cubrirla**, que las clases son
reales y que la cobertura es completa. No garantiza que ese control sea el adecuado. Elegir
qué escanea el shield sigue siendo tu diseño.

Y hay obligaciones de HIPAA que ningún lenguaje puede cubrir, porque no están en el código.
La propia [página de HIPAA de la documentación](https://www.ricardovelit.com/axon-docs/compliance/hipaa)
las enumera: el contrato con cada proveedor que toca la PHI, empezando por el del modelo;
la evaluación de riesgos periódica; el procedimiento de notificación de brechas; las
salvaguardas físicas. AXON no hace que tu sistema cumpla HIPAA. Hace que un tipo concreto
de error, el más fácil de cometer y el más difícil de ver, no pueda llegar a producción.

## Por qué en compilación y no en ejecución

La alternativa habitual son los guardrails de ejecución: un filtro que inspecciona el
tráfico mientras el sistema funciona. Son necesarios, y el shield es justamente eso. El
problema es otro: **si nadie conecta el guardrail, nadie se entera**. Una ruta sin
protección se descubre en producción, si se descubre.

La comprobación en compilación no sustituye al guardrail; garantiza que existe. Son dos
capas que se necesitan:

| | Guardrail de ejecución | Regla de cobertura en compilación |
| --- | --- | --- |
| Cuándo actúa | Mientras el sistema responde | Antes de que exista un artefacto desplegable |
| Qué detecta | Una fuga concreta, en un mensaje concreto | Una frontera sin control, en todo el programa |
| Si falta | Nadie lo nota | El programa no compila |
| Qué deja como evidencia | Registros de lo que bloqueó | Cada build que pasa es una prueba de cobertura |

La última fila es la que más le importa a un auditor. En un sistema normal, demostrar que
todas las rutas que tocan PHI están protegidas exige revisar el código a mano. Aquí, que el
programa compile ya es esa demostración, al menos para la parte que el compilador puede
ver.

## Llévalo a tu caso

La regla completa, con todo lo que demuestra y lo que no, está en la documentación, en
[cumplimiento en compilación](https://www.ricardovelit.com/axon-docs/es/concepts/compile-time-compliance).
Y si tienes un pipeline con datos de pacientes, o con cualquier otra clase regulada, y
quieres ver qué fronteras quedarían al descubierto, [hablemos de tu
arquitectura](https://www.ricardovelit.com/axon).

Si se te ocurre una trampa que compile y no debería, es exactamente el tipo de
hallazgo que quiero leer.

## Para llevarte

- En un pipeline de LLM, la PHI cruza fronteras —el prompt, el proveedor, los logs— que en
  un lenguaje normal ningún tipo ve.
- Si la clase regulatoria forma parte del tipo, quitar la protección deja de ser un
  refactor inocente y pasa a ser un error de compilación.
- La regla resiste las trampas obvias: una etiqueta no cuenta como control, la cobertura
  es una diferencia de conjuntos y el vocabulario es cerrado.
- Lo que no demuestra es que el control sea el adecuado, ni nada de lo que HIPAA pide fuera
  del código. Es una garantía estrecha y exacta, y por eso sirve.
