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.
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) |
| 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.
How a Claude Code session goes
- Claude tries
head -5 data/denials-export.csv. ThePreToolUsehook scans the file, finds a patient table, and denies the command with a message telling Claude why and what to do instead. - 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. - Claude totals the copy and answers. Everything it saw went through phi-gate first.
Install
Claude Code plugin
/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-devskill: 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:redactand/phi-gate:status- a
phi-gatecommand on the agent'sPATH
CLI and git hook (any editor or agent: Codex, Cursor, Copilot, plain git)
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 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):
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.
Try it without an API key
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:
The CLI
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:
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:
Commit checks run in the git pre-commit hook, for any editor or agent:
Audit: every block, warning and override is logged with event, tool, target and category:
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 uses999-), and well-known dummies such as123-45-6789,078-05-1120and repeated digits (111-22-3333) - phone numbers
555-0100to555-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:
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.
[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 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.
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_bytesare blocked unscanned unless you chooseoversize = "scan_head". - The hook runs Python in isolated mode (
-I), so aphi_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=1exists 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 withopaque = "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 recursivegrepover a folder that holds a PHI file is refused until that file is moved, removed or allowlisted. PostToolUsecannot 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 sets out what phi-gate defends against and where that stops. The deployment guide covers rolling it out across a team, and data handling 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
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 for the full checks.
Layout:
.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
Metadata
Release files for phi-gate 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| phi_gate-0.2.0.tar.gz | 134.8 kB | With provenance | Details for phi_gate-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| phi_gate-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for phi_gate-0.2.0-py3-none-any.whl |
Total release size: 224.6 kB
Release files / phi_gate-0.2.0.tar.gz
| Download URL | phi_gate-0.2.0.tar.gz |
|---|---|
| Size | 134.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9d1685440fe64f4e177b473bfc32b177e55aaec9e77ccf0e5b42521ae027664c
|
|
BLAKE2b-256 checksum How to use checksums |
b70e9fab9eb40f2aa67228382b2a53c86d2d0713172584bbca2e6a35d21bc3f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / phi_gate-0.2.0-py3-none-any.whl
| Download URL | phi_gate-0.2.0-py3-none-any.whl |
|---|---|
| Size | 89.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8160b8c3e03a39e5a3d5929f0948596040e5b09a26be9e58955e62b3f4a0b2e6
|
|
BLAKE2b-256 checksum How to use checksums |
067b1ea130305e43cf5b49fe257675a88d6b4a0e2733a6ba238920aee642a150
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for phi-gate 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for phi_gate-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.