Skip to main content

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.

  1. Create the parser file:

    canhoto parsers scaffold --id my_bank_card --type card --institution my_bank
    
  2. See the text the parser receives. This is the extracted text, not the PDF layout:

    canhoto parsers preview --file ~/statements/sample.pdf > sample.txt
    
  3. Edit ~/.canhoto/parsers/my_bank_card.py. The file already reads rows such as 2026-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.
    • _ROW and parse(): match the row format you saw in step 2.
    • _CURRENCY: your ISO 4217 currency code.
  4. 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)

Source distribution for canhoto 0.1.1
File Size Uploaded
canhoto-0.1.1.tar.gz 76.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for canhoto 0.1.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

0.3.0

2 release files

0.2.0

2 release files

0.1.2

2 release files

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page