Metadata-Version: 2.4
Name: phi-gate
Version: 0.2.0
Summary: Fail-closed PHI guardrails for AI coding agents (Claude Code hooks, CLI, git pre-commit)
Author: obutuz
License-Expression: MIT
Project-URL: Homepage, https://github.com/obutuz/phi-gate
Project-URL: Issues, https://github.com/obutuz/phi-gate/issues
Keywords: hipaa,phi,pii,redaction,claude-code,healthcare,dlp
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Typing :: Typed
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Healthcare Industry
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: hypothesis>=6.100; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Dynamic: license-file

# phi-gate

**Fail-closed PHI guardrails for AI coding agents.** A Claude Code plugin, a CLI and a
git pre-commit hook that stop patient identifiers from reaching the model, your code,
your commits, or the tools your agent talks to.

[![CI](https://github.com/obutuz/phi-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/obutuz/phi-gate/actions/workflows/ci.yml)
![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)
[![CodeQL](https://github.com/obutuz/phi-gate/actions/workflows/codeql.yml/badge.svg)](https://github.com/obutuz/phi-gate/actions/workflows/codeql.yml)
![No dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)
![License: MIT](https://img.shields.io/badge/license-MIT-green)

AI coding agents read whatever is in the working tree and send it to a model. On a
healthcare codebase that working tree often holds an eligibility export someone
dropped in `data/`, an 835 remittance file used to debug a parser, an HL7 feed sample,
or seed data that started life as a production query. One `cat` and the agent has sent
patient records to a vendor that may not be covered by your BAA.

phi-gate puts a checkpoint on every path that data can take through an agent session,
and when it cannot run, it blocks instead of letting things through.

## What it guards

| Boundary | How | What happens |
|---|---|---|
| Your prompts | `UserPromptSubmit` hook | A prompt with an SSN, DOB, MRN, member ID... is stopped before it reaches the model (you can deliberately override one prompt, see [Configuration](#configuration)) |
| File reads | `PreToolUse` on `Read`, `NotebookRead`, `Grep` and `Bash` | Denied for files containing PHI. Shell analysis follows `cd` and `pushd`, variables, globs, braces, redirections, `bash -c`, copies, recursive `grep`/`rg`/`find`/`tar`, and paths inside `python -c` or `node -e`. A path it cannot resolve is refused when the project holds PHI |
| Git history | `PreToolUse` on `git show`, `log -p`, `diff`, `stash show`, `grep`, `archive`, ... | The objects the command would print are scanned first, so a deleted file cannot be read back from history |
| Writes and edits | `PreToolUse` on `Write`, `Edit`, `MultiEdit`, `NotebookEdit` | PHI never lands in source, fixtures, seed files or logs |
| Commits | `PreToolUse` on `git commit` and other commit-making commands, plus an optional git pre-commit hook | Each changed file is scanned whole and the commit is judged by the PHI it introduces, so rows appended under an old table header are caught. `--no-verify` (any spelling), aliases, plumbing and `core.hooksPath` overrides are covered |
| Outbound and other tools | `PreToolUse` on every tool: `WebFetch`, `WebSearch`, MCP tools, sub-agent prompts | No identifiers in Slack messages, tickets, URLs, search queries or delegated tasks; files a tool names are scanned too |
| Sending data off the machine | `PreToolUse` on `Bash` | `curl`, `scp`, `nc` and similar are refused when what they send could not be checked |
| Tool output | `PostToolUse` on every tool | Output with PHI is flagged; the agent is told not to propagate it and to tell you |
| Its own rules | `PreToolUse` | The agent cannot edit phi-gate's configuration or audit log, Claude Code settings, `.mcp.json`, or git hooks, so it cannot loosen or switch off its own guardrails |

Every block, warning and override is written to a hash-chained log at
`.phi-gate/audit.jsonl` with the event, tool, target and the categories found. Never
values, never hashes of values, and file names are recorded as keyed hashes by default.
See [data handling](docs/DATA-HANDLING.md).

## How a Claude Code session goes

1. Claude tries `head -5 data/denials-export.csv`. The `PreToolUse` hook scans the file,
   finds a patient table, and denies the command with a message telling Claude why and
   what to do instead.
2. Claude runs `phi-gate redact data/denials-export.csv > copy.csv`. phi-gate's own
   read-only commands are allowed, and the copy keeps the table's shape: identifier
   columns become `[PATIENT_NAME_1]`, `[DOB_1]`, `[MEMBER_ID_1]`, while payer, CARC code
   and amount stay as they were.
3. Claude totals the copy and answers. Everything it saw went through phi-gate first.

## Install

### Claude Code plugin

```text
/plugin marketplace add obutuz/phi-gate
/plugin install phi-gate@phi-gate
```

Needs `python3` (3.11 or newer), `bash` and `git` on the machine. No packages to install.

The plugin adds:

- the hooks above
- the `hipaa-aware-dev` skill: minimum-necessary access, PHI-free logging and error
  reporting, audit trails, synthetic data, vendor/BAA boundaries, with code for
  Next.js, React, Svelte and Python
- `/phi-gate:scan`, `/phi-gate:redact` and `/phi-gate:status`
- a `phi-gate` command on the agent's `PATH`

### CLI and git hook (any editor or agent: Codex, Cursor, Copilot, plain git)

```bash
pipx install git+https://github.com/obutuz/phi-gate
phi-gate install-git-hook          # blocks commits that introduce PHI
phi-gate scan .                    # exit 1 if anything would be blocked
```

With the [pre-commit](https://pre-commit.com) framework (this blocks on any PHI in the
files being committed, including PHI committed earlier, where `install-git-hook` judges
only what the commit introduces):

```yaml
repos:
  - repo: https://github.com/obutuz/phi-gate
    rev: v0.2.0
    hooks:
      - id: phi-gate
```

Other agents can be pointed at the CLI with a rule in their instructions file; see
[`examples/AGENTS.md`](examples/AGENTS.md).

## Try it without an API key

```bash
git clone https://github.com/obutuz/phi-gate && cd phi-gate
./demo/run-demo.sh
```

The script replays the hook calls Claude Code makes against a throwaway project with
synthetic data and prints each decision, then the audit log:

![demo/run-demo.sh output: prompts, reads, writes, guardrail edits and a Slack message, each blocked or allowed](docs/images/demo.png)

## The CLI

```bash
phi-gate scan src/ data/            # human output: path:line:col  severity  category
phi-gate scan --staged --json       # machine output for CI
phi-gate redact export.csv          # placeholder copy on stdout
phi-gate redact export.csv --map m.json && phi-gate restore --map m.json < answer.txt
phi-gate status                     # config and recent audit events
phi-gate audit verify               # exit 1 if the audit log was altered
phi-gate explain                    # every detector and the decision rules
```

**Scan** a project. Output names the file, line, severity and category, never the value:

![phi-gate scan output for a sample billing project](docs/images/scan.png)

**Redact** a file or a stream. Placeholders are typed and consistent within a file, tables
keep their columns, and clinical and financial columns are left alone so the copy is still
useful for analysis:

![phi-gate redact output for a CSV export and a free-text note](docs/images/redact.png)

**Commit** checks run in the git pre-commit hook, for any editor or agent:

![a git commit blocked by the phi-gate pre-commit hook](docs/images/commit.png)

**Audit**: every block, warning and override is logged with event, tool, target and category:

![phi-gate status showing configuration and recent audit events](docs/images/status.png)

## How detection works

Pure Python standard library, no network calls, no model. Detectors are patterns with
validation (check digits, issued ranges, labelled context), plus parsers for the
formats healthcare data actually arrives in. Text files are read as UTF-8, UTF-16 or
UTF-32, and matched on a normalised view, so invisible characters, Unicode dashes and
spaces, fullwidth letters and look-alike letters do not hide an identifier. Every pattern
runs in linear time. Containers are opened and their contents scanned: `.xlsx`, `.docx`, `.pptx`,
OpenDocument files, zip and gzip archives, and SQLite databases (header plus the first
500 rows of each table), because exports are how patient lists actually travel. The scanner,
`phi-gate redact` and the commit gate all read them the same way. A container that cannot
be parsed is blocked, not skipped, and so is a DICOM image, whose header holds the patient's
name.

| Blocks on its own (HIGH) | Blocks next to a clinical fact (MEDIUM) | Informational (LOW) |
|---|---|---|
| SSN (issued ranges), labelled 9-digit SSN | Labelled names | Provider NPI (check digit) |
| Medicare MBI | Phone numbers | DEA number (checksum) |
| Labelled MRN, patient ID, chart number | Email addresses | Public IP addresses |
| Labelled member, subscriber, Medicaid, Medicare IDs | Street addresses, city/state/ZIP | |
| Labelled date of birth | Labelled diagnosis codes (ICD-10) | |
| Patient-labelled names (`patientName`, `Patient Name:`) | Labelled admission, discharge, service dates | |
| Labelled account numbers, Luhn-valid payment cards | | |
| X12 EDI `NM1*QC` / `NM1*IL` and `DMG*D8` segments (835/837) | | |
| HL7 v2 `PID` segments, FHIR `Patient` resources (JSON and XML) | | |
| CSV/TSV/pipe tables with patient identifier columns and data rows | | |
| Labelled values in XML elements (`<SSN>`, `<MemberId>`, `<DateOfBirth>`) | | |

**Decision rules.** Any HIGH finding blocks. A clinical fact (diagnosis code, date of
service) within 400 characters of a name, phone, email or address blocks, because
identity plus health information is what PHI is. Other MEDIUM findings only warn: a
name next to an email is usually a `package.json` author field or a staff directory.
LOW findings never block.

**Synthetic values pass by design.** These cannot belong to a real person, so they are
never flagged and fixtures written with them just work:

- SSNs in never-issued ranges: `000-`, `666-`, `9xx-` (Synthea uses `999-`), and
  well-known dummies such as `123-45-6789`, `078-05-1120` and repeated digits
  (`111-22-3333`)
- phone numbers `555-0100` to `555-0199`, and toll-free numbers
- emails at `example.com`, `example.org`, `example.net`, `.test`, `.example`, `.invalid`
- processor test cards (`4242 4242 4242 4242`, `4111 1111 1111 1111`, ...)

Generated records are different: a Synthea CSV or FHIR bundle still has the shape of a
patient record (name and birth-date columns, `Patient` resources), and phi-gate cannot
tell it from a real export. Keep generator output under a folder listed in `allow_paths`.

### Measured false-positive rate

Blocking ordinary code would get the plugin uninstalled in a day, so this is measured,
not hoped for. Across 4,725 files (69.3 MB: the CPython 3.13 standard library with NumPy
and other installed packages), phi-gate blocked **1 file, or 0.02%**: a 19-digit integer
in NumPy test data that passes the payment card check. Average cost: about 7 ms per
file. Reproduce it on your own code:

```bash
python3 bench/false_positive_sweep.py path/to/your/repo
```

## Configuration

Optional `.phi-gate.toml` at the project root. Everything has a safe default. The
annotated version is [`examples/.phi-gate.toml`](examples/.phi-gate.toml).

```toml
[phi-gate]
mode = "block"                        # block | warn | audit
allow_paths = ["tests/fixtures/**"]   # synthetic data lives here (gitignore-style globs)
allow_values = ["1EG4-TE5-MK73"]      # known test values (exact; prefix "re:" for a regex)
disable = []                          # categories to switch off
opaque = "allow"                      # or "block": refuse formats phi-gate cannot read
```

The [configuration reference](docs/CONFIGURATION.md) covers every key, how patterns
match, and how layers combine.

Start with `mode = "audit"` on an existing repo to see what would be blocked, then
switch to `block`.

**Prompt override.** A prompt that starts with `#phi-ok` is sent even if it contains
identifiers, and the override is logged. Only a person can type a prompt, so the agent
cannot use it. Use it only where your organisation has a BAA covering the model provider,
or switch it off with `prompt_override_token = ""`.

**Managed policy.** A security team can install a policy that every repository on a machine
must meet and none can loosen; a `.phi-gate.toml` in a subfolder can only tighten the
rules. See the [deployment guide](docs/DEPLOYMENT.md).

## Fail-closed by design

- An action is allowed only when the checker exits 0 and confirms the decision with a
  token on a separate file descriptor. If the checker crashes, the interpreter is missing
  or replaced, or the payload is malformed, empty or for the wrong event, the action is
  blocked.
- Claude Code lets an action through when a hook times out, so phi-gate never relies on
  that timeout: detectors are linear-time (tested against adversarial inputs), an
  internal 15-second deadline blocks first, and the wrapper stops the checker at 25
  seconds, before the 30-second hook timeout set in `hooks.json`.
- A typo in `.phi-gate.toml` (unknown key, bad value, invalid regex) blocks with a clear
  error instead of being ignored. An ignored typo in an allowlist is a silent hole.
- Files over `max_bytes` are blocked unscanned unless you choose `oversize = "scan_head"`.
- The hook runs Python in isolated mode (`-I`), so a `phi_gate/` folder in the project
  cannot shadow the real one.
- Findings carry positions, never values, so no message, log line or audit entry can
  leak what was found.
- `PHI_GATE_FAIL_OPEN=1` exists as an explicit, visible escape hatch for a broken
  install. It is off by default, and a managed policy switches it off entirely.

## Limitations

Be clear-eyed about what a pattern scanner on a developer machine can and cannot do.

- **It is not a de-identification tool.** It catches structured identifiers and common
  healthcare formats. It misses names in free-text clinical notes without a label,
  identifiers in PDFs, images, legacy `.doc`/`.xls`, 7z/rar, Parquet and nested
  archives (these are not inspected, or are refused with `opaque = "block"`), and non-US
  identifiers.
- **Shell analysis is static.** Bash is a programming language. phi-gate resolves the
  routes in the table above and refuses what it cannot resolve when the project holds
  PHI, but a program or script file that opens files itself (`python tool.py`, a query
  against a database server) is only checked after it runs, through the output scan. In
  the other direction it is conservative: a recursive `grep` over a folder that holds a
  PHI file is refused until that file is moved, removed or allowlisted.
- **`PostToolUse` cannot un-send.** When output arrives through a route phi-gate could
  not see in advance (a database query, a program that opens files itself), the data is
  already in context. phi-gate then tells the agent to contain it, tells you, and logs
  it. It never sees data a command sent somewhere else.
- **It protects against agent mistakes, not a determined insider.** A user can disable
  the plugin. Pair it with real controls: no production PHI on developer machines,
  least-privilege database access, and your organisation's DLP.
- **"HIPAA-aware", not "HIPAA compliant".** Compliance is a property of an
  organisation's safeguards, policies and agreements, not of a tool. Nothing here is
  legal advice.
- Claude Code shows a blocked prompt back to you so you can edit it. That copy stays
  on your machine; the prompt is not sent to the model.
- This is a Claude Code plugin. claude.ai and Cowork do not install plugins that ship a
  `bin/` folder.

The [threat model](docs/THREAT-MODEL.md) sets out what phi-gate defends against and where
that stops. The [deployment guide](docs/DEPLOYMENT.md) covers rolling it out across a team,
and [data handling](docs/DATA-HANDLING.md) what phi-gate stores and where.

## Roadmap

- Optional named-entity recognition for names in free text, behind a flag, keeping the
  standard-library default
- Remaining Safe Harbor identifiers: license, vehicle and device numbers
- Pluggable detector packs for non-US identifiers
- Windows support, which today is blocked by the bash hook wrapper
- Direct audit shipping to a SIEM
- Publishing to PyPI, and an independent security review before 1.0

## Development

```bash
python3 -m unittest discover -s tests     # unit, hook, git, property and stress tests
./demo/run-demo.sh                        # exits 1 if any decision changes
claude plugin validate . --strict
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for the full checks.

Layout:

```text
.claude-plugin/     plugin.json and marketplace.json (this repo is its own marketplace)
hooks/              hooks.json and the fail-closed run.sh wrapper
bin/phi-gate        CLI, put on the agent's PATH by Claude Code
src/phi_gate/       detectors, policy, scanner, shell analysis, redaction, audit, hooks, CLI
skills/             hipaa-aware-dev skill and references
commands/           /phi-gate:scan, /phi-gate:redact, /phi-gate:status
tests/              unit, hook, git, property and stress tests (synthetic data only)
bench/              false-positive sweep
docs/               threat model, deployment, configuration, data handling, screenshots (synthetic data only)
```

## License

MIT
