German freelancer tax filing for Claude Code. Fetch transactions from your bank, classify them with AI, calculate the EÜR, and walk through ELSTER step by step.
This plugin is for Freiberufler (German self-employed professionals — §18 EStG) who want to prepare their annual tax return without doing all the data wrangling by hand. It currently supports Wise as a transaction source, with a generic CSV importer for everything else, and a clean extension point for adding more banks.
After running the three skills for a given year, your working directory contains:
| File | What it is |
|---|---|
verdicts-<YEAR>.json |
The verdict map: one entry per merchant group, keyed by a substring of the merchant name. This is the hand-editable audit trail. Edit it, then re-run parse-statements to re-gate and re-freeze classified.json — never hand-edit classified.json itself. |
steuer-<YEAR>-classified.json |
Every transaction and every netted merchant group, with the verdict baked in. The frozen artifact calculate-euer reads — regenerated by parse-statements, not edited directly. |
steuer-<YEAR>.pdf |
Formatted EÜR report with totals, ready for the Finanzamt. |
steuer-<YEAR>.csv |
Every row with its EUR equivalent and ECB rate. |
steuer-<YEAR>-summary.json |
Totals + Gewinn + by_category + excluded breakdown. |
Plus an interactive ELSTER walkthrough: you paste a screenshot of each form page, the plugin tells you which value goes in each field.
The plugin runs locally inside your Claude Code session. There are no flykit servers, no telemetry, nothing phones home. Wise API calls go directly from your machine to api.wise.com; ECB reference rates come directly from the ECB's own data API (cached to disk per year, so re-runs are offline). Your bank token lives in a local .env file and never leaves your machine.
- Node.js >= 18
npmon yourPATH(the plugin installs its own dependencies on first run)
In Claude Code:
/plugin marketplace add flykit-cc/plugins
/plugin install steuer@flykit
First invocation of any /steuer:* skill installs npm dependencies automatically. Subsequent runs are instant.
Steuer acts on your current working directory — it reads config from there and writes reports back to the same place. You don't need a "steuer project" or a repo; just pick a folder where you want your tax data to live:
mkdir -p ~/taxes/2024 && cd ~/taxes/2024
claude
# inside Claude Code:
/steuer:parse-statements 2024Two ways to get transactions in. Pick one per run with --source; default is Wise.
| Wise API (default) | CSV import | |
|---|---|---|
| How | Plugin calls api.wise.com with your read-only token |
You export a CSV from your bank's UI and point the plugin at it |
| Coverage | Full year, all profiles, incl. third-party deposits (e.g. Deel wiring money in), card payments, direct debits | Whatever the export includes — usually just transfers |
| Setup | One token, pasted once into .env |
Re-export every time you re-run |
| Account types | Personal Wise accounts only (see below) | Any bank that exports CSV |
Why the Wise API beats the CSV. Wise's standard CSV export omits activity that isn't a transfer out of your balance — including inbound third-party payouts (payroll platforms, client bank wires), card spend, and direct debits. The API sees all of it. If you only use CSV, you will silently miss income and expenses.
Current Wise limitation. The client works with personal Wise accounts (where SCA/PSD2 is not enforced on the API). Business accounts have SCA complications that this client does not currently handle. Patches welcome.
CSV fallback. Useful if you're on a non-Wise bank or want to feed in a one-off statement. See scripts/sources/csv-import.js for the accepted columns (date, description, amount, currency).
Create a .env file in the directory you run Claude Code from:
# Required if you use the Wise source
WISE_API_TOKEN=your_token_here
# Optional — printed in the PDF report header (set any subset)
ACCOUNT_NAME=Your Name
ACCOUNT_BANK=Your Bank
ACCOUNT_BANK_ADDRESS=1 Example Street, City, Country
ACCOUNT_TYPE=Checking
ACCOUNT_ROUTING=SWIFT/BIC or routing number
ACCOUNT_NUMBER=IBAN or local account numberGetting a Wise token. wise.com → Settings → API tokens. Read-only scope is enough — the plugin never writes to Wise. The token stays in your local .env; it's read by process.env.WISE_API_TOKEN on your machine and used only to call api.wise.com directly.
PDF header fields. Only the fields you set are rendered; nothing appears blank.
| Env var | When to set |
|---|---|
ACCOUNT_NAME |
Always — shown as the centered title of the PDF. |
ACCOUNT_BANK |
If the Finanzamt needs to see which institution holds the account. |
ACCOUNT_BANK_ADDRESS |
When the bank's postal address matters (e.g. foreign banks). |
ACCOUNT_TYPE |
To distinguish personal vs. business / checking vs. savings. |
ACCOUNT_ROUTING |
ABA / SWIFT / BIC / sort code when relevant. |
ACCOUNT_NUMBER |
IBAN or local account number for the header. |
Optional non-secret preferences (default year, source, output dir) can live in ~/.config/flykit/steuer/config.json:
{
"default_year": 2025,
"default_source": "wise",
"output_dir": "./output"
}The three skills form a pipeline:
- parse — fetch raw transactions, filter → net → detect cross-source overlap → look each merchant group up in the verdict map → reconcile. Anything
MISSINGgets resolved (by the model or by you) until every row is accounted for. - calculate — refuses to run until step 1 reconciled clean; converts currencies, totals by verdict code, emits the PDF / CSV / summary.
- elster-guide — reads the summary and walks you through the forms.
Fetches a year of transactions from the configured source, drops rows that never moved money (pre-auth holds, memo rows), nets each merchant group against its own reversals, detects the same spend appearing in two sources, and looks each group up in verdicts-<YEAR>.json. The skill reads every MISSING group (largest amount first), decides what it can, and asks you the rest one at a time — then re-runs until the reconcile check passes.
Output:
./output/verdicts-<YEAR>.json— the verdict map. This is the hand-editable audit trail: one entry per merchant group ({ code, category, share? }), written once and applied forever after. Disagree with a verdict? Edit this file, then re-runparse-statementsto re-gate and re-freezeclassified.json../output/steuer-<YEAR>-classified.json— every transaction and every netted group, verdict baked in. Regenerated each run — don't hand-edit this one.
Flags.
--profile <all|personal|business>(Wise only, defaultall) — restrict fetching to one Wise profile type. Unknown values and empty result sets fail fast listing what's available.--manual-expenses <path>— merge a JSON array of extra expense entries into the run. Each entry must havedate(YYYY-MM-DD),description,amount(positive number), andcurrency(EURorUSD). Useful for cash receipts, non-Wise direct debits, or expenses your bank export misses.--source-label <name>— relabel this run's source (e.g. label a PayPal CSV importpaypalso it matchesPAYPAL *...card descriptions during overlap detection).--prefer-source <name>— when the same spend is detected in two sources, keep the named source's row.--verdicts <path>— verdict file location (default<output>/verdicts-<YEAR>.json).--force— allow writing a verdict map with fewer entries than the one already on disk (the shrink-guard blocks this otherwise).
[
{ "date": "2024-07-03", "description": "Cash receipt — printer paper", "amount": 12.40, "currency": "EUR" }
]Reads the classified JSON, converts every USD row to EUR using ECB reference rates fetched directly from the ECB's data API, and totals by verdict code — I for income, B in full and A at its stored share for expenses; P/V/N/NI/M/H excluded. Writes:
./output/steuer-<YEAR>.pdf— formatted report for the Finanzamt./output/steuer-<YEAR>.csv— every row with its EUR equivalent and rate./output/steuer-<YEAR>-summary.json— totals, Gewinn,by_category, andexcluded
Refuses to run (exit code 2, listing the offending groups) while any group is still MISSING or coded R — go back to parse-statements to resolve them. Pass --include-review to admit R-coded groups on the income side only; MISSING always blocks regardless of flags.
Walks you through filling in each ELSTER form. You paste a screenshot or text of the page; the skill identifies the form, looks up your numbers in the summary JSON, and tells you exactly which value goes in each field.
plugins/steuer/
├── skills/ Skill definitions (what users invoke)
│ ├── parse-statements/SKILL.md
│ ├── calculate-euer/SKILL.md
│ └── elster-guide/SKILL.md
├── scripts/
│ ├── sources/ Bank-agnostic transaction sources
│ │ ├── wise.js
│ │ ├── csv-import.js
│ │ └── README.md Source contract — read this to add a new bank
│ ├── netting.js Group by merchant, net reversals against holds
│ ├── verdicts.js Verdict map: load/save/lookup (longest-match), MISSING reporting
│ ├── overlap.js Cross-source duplicate detection
│ ├── reconcile.js Full-coverage row-count assertion
│ ├── rateConverter.js ECB reference rates, fetched directly from the ECB data API
│ ├── classifier.js Rule-based income classifier — suggestion only; verdicts.js is the source of truth
│ ├── pdfGenerator.js PDF report
│ ├── csvGenerator.js CSV report
│ ├── parse-statements.js CLI: filter -> net -> overlap -> verdicts -> reconcile
│ ├── calculate-euer.js CLI: verdict-aware EÜR totals, gates on MISSING/R
│ └── lib/config.js Config loader
└── references/ Generic reference docs (no personal data)
├── tax-categories.md
├── ecb-methodology.md
└── elster-fields.md
- Create
scripts/sources/<bankname>.js. - Export
async function fetchTransactions({ year, ...opts })returning{ income: [], expenses: [] }. - Each transaction:
{ date, description, amount, currency, source, raw }(positiveamount,date=YYYY-MM-DD). - See
scripts/sources/README.mdfor the full contract. - Wire it up by passing
--source <bankname>to the CLI scripts.
This plugin is a tool, not tax advice. It automates data collection, currency conversion, and form-field lookup, but it does not replace a Steuerberater. Always verify your numbers against the actual ELSTER form and consult a tax professional for anything non-obvious. The maintainer is not responsible for filing errors, penalties, or audit outcomes.
This is self-application tooling the maintainer built and uses for their own filing — not a tax product, and not audited for anyone else's situation. Review every number it produces before filing.
The plugin does not store personal data (StNr, IdNr, name, address, bank details). Keep those in a private file outside the plugin.