Canhoto
Canhoto keeps your bank and card statements on your computer. It parses statements, categorizes spending, asks you about uncertain items, and exports a monthly PDF summary.
Using an AI agent? Connect
canhoto-mcp. The agent gets a bounded set of tools to parse statements, review and categorize transactions, read monthly totals, and export reports. It never gets SQL access or a full ledger dump. The CLI gives you the same workflow by hand.
| Package | canhoto |
| CLI | canhoto |
| MCP | canhoto-mcp |
| Data | ~/.canhoto or $CANHOTO_DATA_DIR |
Install
Install the CLI and the MCP server from PyPI:
uv tool install canhoto
Or install from a checkout of this repository:
uv tool install .
Or run it in place:
uv sync
uv run canhoto --help
Then create the data directory and check it:
canhoto init
canhoto doctor
How it works
flowchart TD
A[Statement PDF or text] --> B[Parser]
B --> C[Ingest]
C --> D[(Local SQLite ledger)]
D --> E[Your rules, built-in rules, merchant memory]
E --> F[Review uncertain items]
F --> G[Apply categories]
G --> H[Monthly breakdown]
H --> I[Summary PDF]
Parsers only read statement rows. Canhoto handles categories, rules, merchant memory, reports, and exports.
Create a parser
Canhoto does not ship bank-specific parsers. A parser is one small Python file
in ~/.canhoto/parsers/. It reads the text of a statement and returns its rows.
You do not need this repository to write one.
-
Create the parser file:
canhoto parsers scaffold --id my_bank_card --type card --institution my_bank
-
See the text the parser receives. This is the extracted text, not the PDF layout:
canhoto parsers preview --file ~/statements/sample.pdf > sample.txt
-
Edit
~/.canhoto/parsers/my_bank_card.py. The file already reads rows such as2026-06-02 ACME STORE -12.34, and its comments list the rules. Change three things:sniff(): return a score above 0 only for text unique to this statement._ROWandparse(): match the row format you saw in step 2._CURRENCY: your ISO 4217 currency code.
-
Test the parser, then enable it:
canhoto parsers test --id my_bank_card --file ~/statements/sample.pdf canhoto parsers enable --id my_bank_card
The test passes only when parse() returns at least one transaction and
sniff() claims the sample. A failed test disables the parser until it passes
again.
Keep transaction ids stable: build them from the bank's operation id, or from
the date, amount, and description. Never use the row position, because then
two statements can overwrite each other's rows. Leave category and kind
empty. Your rules and review set them.
For password-protected PDFs, pass --pdf-password or set
CANHOTO_PDF_PASSWORD. For a complete example, see
examples/parsers/.
An agent connected to canhoto-mcp can also write the parser for you. See
the MCP section below.
Process a month
# Read statements into the ledger.
canhoto ingest ~/statements/*.pdf
# Apply your rules, the built-in rules, and merchant memory.
canhoto categorize rules --month 2026-06
# List the items that still need a decision.
canhoto review --month 2026-06
# Apply your decisions.
canhoto categorize apply --file patches.json
# See income, expenses, and category totals.
canhoto breakdown --month 2026-06
# Write the PDF summary.
canhoto export pdf 2026-06
patches.json is a list of changes, one per transaction id from review:
[
{ "id": "my_bank_card-2026-06-02-0001", "category": "Groceries",
"kind": "expense", "is_expense": true, "needs_review": false }
]
Canhoto marks every row you change this way as manual. No automatic rule
changes a manual row again.
The PDF goes to ~/.canhoto/exports/2026-06-summary.pdf by default. It shows
totals by category and the top merchants in each category. It never contains a
transaction list or raw statement descriptions.
Teach Canhoto your rules
When a counterparty always means the same thing, store a rule. categorize rules applies it to every future statement.
canhoto rules add --pattern "PIX RECEBIDO ACME LTDA" --direction in \
--min 1200 --max 1300 --category Income --kind income \
--note "Monthly pay from my company"
canhoto rules list
canhoto rules remove --id 3
| Option | Meaning |
|---|---|
--pattern |
Regular expression, case-insensitive, matched against the description. |
--direction |
in (money received), out (money sent), or any. |
--min, --max |
Amount range, inclusive, without the sign. Both are optional. |
--source-kind |
Match only account or card rows. |
--category |
The category to set. |
--kind |
expense, income, transfer, internal_transfer, self_transfer, or card_payment. Transfers do not count as income or spending. |
--review |
Set the category, but keep the row in the review queue. Use this when an amount range cannot separate two cases. |
--note |
Why the rule exists. Agents read it during review. Do not put secrets in it. |
--priority |
Lower numbers run first. The first matching rule wins. |
Your rules run before the built-in rules. They never change manual rows. When you upgrade to the version with rules, Canhoto marks your existing reviewed rows as manual.
To remember one merchant without a full rule, use merchant memory:
canhoto categorize merchant --key CURSOR --category Subscriptions
PDF profiles
Choose a built-in style:
canhoto export pdf 2026-06 --profile canhoto
canhoto export pdf 2026-06 --profile modern --output ~/Documents/2026-06.pdf
canhoto export pdf 2026-06 --profile minimal
canhoto: receipt-style report with a category chart.modern: clean report with metric cards and a category chart.minimal: text-only report without a chart.
MCP
The CLI and the MCP server use the same service layer. For agent-assisted use, start the MCP server. The agent follows this flow:
statement_preview -> parser_* -> ingest -> rule_list -> run_rules ->
review_batch -> set_categories -> month_breakdown -> export_pdf
When you explain a recurring counterparty to the agent, it can store it with
rule_add. The MCP server exposes only domain tools. It does not give SQL
access or full ledger dumps.
To let an agent write parsers, add this to ~/.canhoto/config.json:
{ "agent_view": { "allow_parser_writes": true } }
Example MCP host configuration:
mcp_servers:
canhoto:
command: canhoto-mcp
CLI commands
canhoto init | doctor
canhoto parsers scaffold|preview|test|enable|list
canhoto ingest <files...> [--pdf-password PASSWORD]
canhoto categorize rules --month YYYY-MM
canhoto categorize apply --file patches.json
canhoto categorize merchant --key KEY --category CAT
canhoto rules add|list|remove
canhoto review --month YYYY-MM [--cursor ID] [--limit N]
canhoto breakdown --month YYYY-MM
canhoto export pdf YYYY-MM [--profile canhoto|modern|minimal] [--output PATH]
Develop
uv sync --extra dev
uv run pytest -q
uv run ruff check src/canhoto tests
uv run mypy -p canhoto
Or use the justfile with Just.
Release
Pushing a version tag publishes to PyPI through
.github/workflows/release.yml. The workflow
tests, builds, smoke-tests the wheel and the source distribution, and publishes
with PyPI Trusted Publishing. The tag must match the version in
pyproject.toml.
uv version --bump patch # or minor / major
git commit -am "chore: release v$(uv version --short)"
git tag -a "v$(uv version --short)" -m "v$(uv version --short)"
git push origin main --tags
Privacy
Your statements and database stay in the data directory. Do not commit
statements, tokens, database files, or ~/.canhoto. Back up canhoto.db before
you upgrade: it holds your ledger and your rules.
Metadata
Release files for canhoto 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| canhoto-0.1.1.tar.gz | 76.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| canhoto-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 134.9 kB
Release files / canhoto-0.1.1.tar.gz
| Download URL | canhoto-0.1.1.tar.gz |
|---|---|
| Size | 76.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9bad70869c6900e04a4ad89b4ccd93758b45c3396fb2ec4c976e6c0cd9b946c1
|
|
BLAKE2b-256 checksum How to use checksums |
8cd45e0569ba3a568ad39910341b13edd7cb0543b5bddfa28f137dc78cb8e620
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
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 Oct 4, 2026.
Transparency logRelease files / canhoto-0.1.1-py3-none-any.whl
| Download URL | canhoto-0.1.1-py3-none-any.whl |
|---|---|
| Size | 58.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c9fe7c4e0174e3dd74067ee33960df3fa8b6d6d7bf4edbe609beac2e7c82305e
|
|
BLAKE2b-256 checksum How to use checksums |
7bfbccda4eb057e5096cd90916c49c548e2aedd1869a153d228c640d81e59850
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
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 Oct 4, 2026.
Transparency log