downcoding-defense is the open detection + appeal-letter engine behind
Receipts Health. It runs fully self-hosted: a local 835 file in, a PDF out. We
take the security of both the code and the people who run it seriously.
Please do not open a public issue for security vulnerabilities.
Report privately to security@receiptshealth.com. Include:
- a description of the issue and its impact,
- the affected version or commit,
- steps to reproduce (a minimal proof of concept if you have one), and
- any suggested remediation.
You can expect an acknowledgement within 3 business days and a substantive response within 10 business days. We practice coordinated disclosure: we'll work with you on a fix and a disclosure timeline, and we're happy to credit you in the release notes unless you'd rather stay anonymous. Please give us a reasonable window to ship a fix before any public disclosure.
If you prefer encrypted email, say so in a first message and we'll exchange keys.
In scope — anything in this repository: the 835 parser, de-identification boundary, detection rules, the LLM classifier and letter generator, the standards index, PDF generation, and their handling of untrusted input (a malformed or malicious 835, a crafted CSV, prompt-injection via file contents, citation-token validation, path handling, dependency risk).
Out of scope — the hosted Receipts Health service. Authentication, secrets, Cloud SQL, the operational PHI boundary, and job orchestration live in a separate, private deployment plane and are not part of this repo. Report issues in the hosted product to the same address, but note that its code is not published here.
Open code is not an open system. This repo is public on purpose — auditability and "many eyes" are a security asset, and the AGPL forces hosted forks to publish their changes — but the sensitive material stays out:
- No credentials or secrets. Every key is read from the environment at run
time (see
.env.examplefor the variable names; the values are yours). None are committed. - No PHI. The engine de-identifies at the parse boundary before any LLM
call; identifiers are templated back into letters locally, after the model
call. The committed 835 test corpus is synthetic — a
no-PHIrule is enforced by policy intests/fixtures/corpus/README.mdand bytests/test_deidentify_corpus.py. If you ever find real patient data in this repo or its history, treat it as a security incident and report it privately.
- Treat every input 835/CSV as untrusted; run in an environment you control.
- Set
MOCK_EXTERNAL=trueto exercise the full flow offline with no network and no LLM call. - Keep dependencies current (
uv sync); we monitor advisories for the pinned set.