Keep-search redaction for bank-statement PDFs. You name what stays visible. Everything else is burned out of the file.
A landlord, auditor, or reimbursement packet does not need your grocery run, your payroll, or your account number. It needs Hostinger on 03/02 for $4.23. Redactus finds those rows, keeps date / description / debit or credit, blanks the running balance and every other transaction, then asks which header fields to take with them.
This is true redaction. Text is removed from the PDF content stream. A black rectangle painted on top is not redaction. Blur is not redaction. If you can copy-paste it, it is still there.
Synthetic layout. Real statements never ship in this repo.
flowchart LR
A[PDF] --> B[Scan / map layout]
B --> C[Find keep terms]
C --> D[Ask PII checklist]
D --> E[Add or remove items]
E --> F[One-shot from map]
F --> G[Verify]
- Scan first. Each statement is mapped before anything burns: header, columns, date style, split-amount quirks, continuation merchants, masked fields.
ready: yesis the gate. - Keep-search.
Hostingermatcheshostinger.com,HOSTINGER *DOMAINS, and USAARECURRING DEB CARD PURCHcontinuations. - Kept rows retain date, description, and the debit or credit. Running balance is blank by default.
- Every other transaction is boxed as a whole row, including merchant continuations (
SHEETZ … WAKE FOREST NC). - Standard fields are a checklist, not a surprise: account number, routing, bank name/address, holder name/address, phone, email, member number, statement period, beginning/ending balance, period totals. The map tells you which of those this PDF actually has.
- Add or remove at any time. Another merchant, one match index, extra text on a kept row (
Larnaka), or “leave the bank name.” Re-plan is ~0.3s. You are not stuck with the first pass. - Verify fail-closed. Kept terms must still extract. Distinctive tokens from blanked rows must not.
Engine: PyMuPDF (add_redact_annot + apply_redactions). There is no JEV CLI in this tree. Overlay-only tools are out of scope.
Python 3.12 or newer. PyMuPDF has wheels; no system PDF toolkit required.
pipx install redactus
# or
pip install redactusFrom a clone (development):
git clone https://github.com/mrdulasolutions/redactus.git
cd redactus
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e .Cursor users: the Agent skill at .cursor/skills/bank-statement-redact/ drives the same redactus command.
# Map this statement first (header, columns, quirks, masked fields)
redactus scan statement.pdf --keep Hostinger --workdir /tmp/redactus -o /tmp/redactus/map.json
# Burn a copy from that map. Original is never overwritten.
redactus redact statement.pdf --map /tmp/redactus/map.json \
--keep Hostinger \
--redact-pii account_number,routing_number,running_balance,beginning_balance,ending_balance,period_totals,phone \
-o statement_hostinger.pdf \
--workdir /tmp/redactuspython -m redactus is the same CLI.
find prints indexes. Use those when you want one hit gone and the rest kept.
[72] p6 03/02 RECURRING DEB CARD PURCH … hostinger.com Larnaka $4.23
[79] p6 03/02 RECURRING DEB CARD PURCH … hostinger.com Larnaka $16.36
Extract once. Re-plan as the list changes.
| You want | Flag |
|---|---|
| Also keep Apple | --keep Hostinger --keep Apple |
| Blank one match | --unkeep-index 79 |
| Extra blank on kept rows | --also-redact Larnaka |
| Leave bank name visible | omit bank_name from --redact-pii |
| Blank the statement period too | add statement_period to --redact-pii |
| Keep running balance | --keep-running-balance |
redactus plan /tmp/extract.json \
--keep Hostinger --keep Apple \
--unkeep-index 79 \
--also-redact Larnaka \
--redact-pii account_number,routing_number,running_balance,beginning_balance,ending_balance,period_totals \
-o /tmp/plan.json
redactus preview statement.pdf /tmp/plan.json -o /tmp/preview
redactus apply statement.pdf /tmp/plan.json -o statement_hostinger.pdf --engine pymupdf
redactus verify statement_hostinger.pdf /tmp/plan.jsonRecommended yes unless you have a reason: account number, routing, phone, email, SSN/ITIN, running balance, beginning balance, ending balance, period totals. Totals leak the hidden activity. A $12.99 Hostinger debit next to an ending balance of $37,225.45 is a tell.
Asked with no default yes: bank name, bank address, account holder name, account holder address, statement period.
Full field ids: .cursor/skills/bank-statement-redact/pii-fields.md.
Timed on a 14-page USAA Classic Checking PDF (~177 transactions, 7 keep hits), local SSD, Python 3.12:
| Step | Wall time |
|---|---|
| scan / map | 0.2–0.5 s |
| extract | 0.2–0.4 s |
| find | ~0.1 s |
| plan | ~0.1 s |
| apply (PyMuPDF burn) | ~0.2 s |
| verify | ~0.1 s |
one-shot redact |
0.3–0.7 s |
| revise (plan + apply, no re-extract) | ~0.3 s |
Nothing is uploaded. The statement never leaves the machine.
USAA Classic Checking splits one visual row across PDF text objects: the Debits/Credits 0 sits ~1.5 pt above the date line, dates are MM/DD, and hostinger.com Larnaka is a continuation under DEBIT CARD PURCHASE. Redactus clusters by Y-midpoint so that still parses as one transaction.
Chase / BofA-style single-line tables with Date Description Debit Credit Balance headers work on the same path. Image-only scans are refused until OCR exists; fail closed beats a silent miss.
A synthetic statement is built in:
redactus sample -o /tmp/sample.pdf
redactus scan /tmp/sample.pdf --keep Hostinger --workdir /tmp/sample-work -o /tmp/sample-work/map.json
redactus redact /tmp/sample.pdf --map /tmp/sample-work/map.json --keep Hostinger \
--redact-pii account_number,routing_number,running_balance,beginning_balance,ending_balance,period_totals \
-o /tmp/sample_redacted.pdf --workdir /tmp/sample-work- Original PDF is never overwritten. Output is always a copy.
- Metadata is stripped on burn.
- The CLI prints last-4 masks, not full account or routing numbers.
- Do not paste live statements, full account numbers, or non-kept merchants into chat logs.
verifyis the contract: if it exits non-zero, do not deliver the file.
This is a local tool for documents you own. It is not legal advice and it is not a substitute for a records-retention policy.
src/redactus/ # packaged CLI (`redactus`, `python -m redactus`)
cli.py # scan / extract / find / plan / apply / verify / redact
pyproject.toml
.cursor/skills/bank-statement-redact/
SKILL.md # Cursor Agent playbook (same CLI)
pii-fields.md
LICENSE # MIT
MIT. Copyright (c) 2026 M.R. Dula.
