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.

Keep manual transactions

Some money moves outside every statement you can export, for example a Pix sent straight from a savings reserve. Keep those rows yourself:

canhoto manual add --date 2026-09-17 --amount -3080.00 \
  --description "Pix enviado Example Tax Office" --category Taxes \
  --merchant IPVA --account cofrinho --note "Car tax, paid from the reserve"
canhoto manual list [--month 2026-09]
canhoto manual edit --id manual-... --amount -3100.00 --category Taxes
canhoto manual remove --id manual-...

A negative amount is money out. With --category, the row is set by hand. Without it, your rules classify the row, or it waits in review. Canhoto refuses to add the same date, amount, description, and account twice. An edit keeps the id. edit and remove refuse statement rows: those change only when you ingest the statement again.

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.

Back up and restore

Write your ledger, rules, merchant memory, config, and parsers to one file:

canhoto backup                        # ./canhoto-YYYY-MM-DD.canhoto
canhoto backup --output ~/backups/home.canhoto

Restore it into a new or empty data directory. Do not run canhoto init there first:

CANHOTO_DATA_DIR=~/.canhoto-new canhoto restore ~/backups/home.canhoto

A .canhoto file is a zip archive. Open it with any unzip tool:

File Content
manifest.json Format version, Canhoto version, schema revision, row counts, SHA-256 of each file
canhoto.sql Plain SQL dump of canhoto.db: schema and rows. sqlite3 new.db < canhoto.sql rebuilds it.
config.json Settings and the parser registry
parsers/*.py Your parser modules

The backup does not include raw statements. A restored ledger cannot re-parse old statements until you ingest them again.

Restore checks every checksum and the schema revision before it writes. It prepares and migrates the ledger in a private temporary directory. It installs the full restore only after all files are ready. A failed restore permits a retry. Restore refuses non-empty directories and directory links. It keeps parser modules in the restored directory's parsers/ folder, even when the backup used a custom parser location.

Restore only backups you trust. Checksums do not authenticate the sender. The backup includes executable parser modules and holds your full ledger in plain text, so keep the file private. backup and restore are CLI only: the MCP server never gives an agent a full ledger dump.

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]
canhoto manual add --date YYYY-MM-DD --amount SIGNED --description TEXT [--category CAT]
canhoto manual list [--month YYYY-MM] | edit --id ID [fields] | remove --id ID
canhoto backup [--output PATH]
canhoto restore 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, backups, or ~/.canhoto. Run canhoto backup before you upgrade: the database holds your ledger and your rules.

Metadata

Release files for canhoto 0.2.0

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.2.0
File Size Uploaded
canhoto-0.2.0.tar.gz 89.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for canhoto 0.2.0
File Interpreter ABI Platform
canhoto-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 156.8 kB

Release files / canhoto-0.2.0.tar.gz

Download URL canhoto-0.2.0.tar.gz
Size 89.3 kB
Tags Source
SHA-256 checksum
How to use checksums
f504b2d203c5a6ce63d15a89fc402b94371a4bcdd25715b3fe4150e7874d018e
BLAKE2b-256 checksum
How to use checksums
b5b5d2b31307cc7ac54f2eb2609e64ca9ee03f8df8006544c9d7491da8c21b0f
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 7, 2026.

Transparency log

Release files / canhoto-0.2.0-py3-none-any.whl

Download URL canhoto-0.2.0-py3-none-any.whl
Size 67.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d2817d3d9df0979ae7d892511554b85681af73ab8c066a7273570c19723609cc
BLAKE2b-256 checksum
How to use checksums
8d5433cf5dcf8df871f50605a6629187ce3b4d07e9cb70570da276d716d5ac11
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 7, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.0

2 release files

This release

0.2.0 This release

2 release files

0.1.2

2 release files

0.1.1

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