From Peat Bakke Β· peat@peat.org Β· Bluesky @peat.org
Fair warning: this is a very nerdy work in progress. It describes a system I built for my own household and my own small business, and it reflects my situation, my banks, and my tolerance for plain-text ledgers. It is not accounting, tax, or legal advice. If you run with it, you do so at your own risk, and please keep a real CPA in the loop.
That said, I'm happy to chat. Questions, feedback, and "here's what broke" reports are all welcome by email or on Bluesky.
A reconstruction guide for building a personal + small-business bookkeeping system that Claude Code operates. Give this whole document to Claude Code in an empty repository and work through it phase by phase (see Part 15, Build order).
The system this describes has run a real household and a single-owner consulting company for most of a year: plain-text double-entry ledger, scripted bank feeds, a reconciliation loop that proves the ledger matches the bank to the cent, envelope budgeting, a tax-reserve calculator, payroll splitting, an accountable-plan reimbursement routine, and tax-season document prep. Every private figure has been removed. What remains is the design, the procedures, the file formats, the script contracts, and the rules Claude follows.
Prerequisites
- Python 3.12 and a virtualenv.
- Beancount v3 (
beancount,beanquery,fava). - At least one bank feed: a SimpleFIN Bridge subscription (covers most US banks and brokerages), and/or a bank with a real API (Mercury has a CLI and API), and/or CSV exports.
git,pdftotext(poppler) if you will parse paystubs.- Claude Code with a
CLAUDE.mdin the repo root.
Conventions in this document
<angle brackets>are placeholders to fill in.- "Owner" is the human; "Claude" is Claude Code acting as bookkeeper.
- "The business" is a pass-through entity (LLC, S-Corp) owned by the Owner. If you have no business, delete the
Businesssubtree and the payroll/K-1 sections. - Tax rules referenced are US federal plus one state. Adapt to yours. This playbook is bookkeeping engineering, not tax advice; keep a CPA.
These are the load-bearing rules. Everything else derives from them. Put them at the top of CLAUDE.md in some form.
- The ledger is the source of truth. One plain-text Beancount file, in git. Bank feeds, spreadsheets, and statements areinputs that the ledger is reconciled against, never the record itself.
- Scripts fetch. Claude proposes. The Owner approves. Then Claude records. Claude never writes a transaction to the ledger without the Owner reviewing it first. This one rule is what makes the whole system trustworthy.
- Cent accuracy is proven, not assumed. Transaction matching proves entries are not duplicated or invented. Only a balance check against the bank's own reported balance proves nothing ismissing . Both run every session.
- Balance assertions come from the bank, never from the ledger. Copying the ledger's own number into a
balancedirective certifies your own error. A dropped grocery charge hid for three months this way before the rule was made absolute. - Residuals get investigated, never plugged. When bank and ledger disagree, find the real missing or wrong entry. No
Equity:Adjustmentsplugs to make a number go away. - No live figures in
CLAUDE.md. Balances, year-to-date totals, and receivables are derived.CLAUDE.mdholds procedure, policy, profile facts, and open items. A script (status.py) reports the numbers. - Fixed terminology. One term, one meaning. Define the handful of house terms in a table at the top of
CLAUDE.mdand never introduce synonyms. Drift in vocabulary becomes drift in bookkeeping. - Git is the archive. Delete expired content outright. No
docs/archive/, no commented-out history.git logrecovers anything. - Round numbers for planning, exact numbers for the ledger. Budget and strategy discussions use round figures; ledger entries are exact to the cent.
- Monthly cadence is non-negotiable for anything with a legal clock (accountable-plan reimbursements, estimated taxes, payroll filings). The bookkeeper's job includes nagging.
finances/
βββ CLAUDE.md # Claude's instructions: role, workflows, policy, open items
βββ RULES.yaml # Transaction categorization patterns
βββ BUDGET.yaml # Envelopes, fixed costs, payroll model, tax reserve config, runway
βββ requirements.txt
βββ .gitignore
βββ .claude/
β βββ settings.local.json # Permission allowlist (git add/commit, bean-check, python, etc.)
β βββ skills/
β βββ reconcile-<bank>/SKILL.md # One skill per feed with recording quirks
β βββ reconcile-<receivable>/SKILL.md
βββ ledger/
β βββ main.beancount # THE source of truth
βββ scripts/
β βββ common.py # Paths, account groups, beanquery helpers, DataSyncer
β βββ simplefin.py # SimpleFIN API client
β βββ sync.py # SimpleFIN β data/simplefin/YYYY-MM-DD.json
β βββ <bank>.py # Bank API client (e.g. mercury.py)
β βββ <bank>_sync.py # Bank API β data/<bank>/YYYY-MM-DD.json
β βββ <receivable>.py # Spreadsheet-tracked loan/receivable β data/<receivable>/
β βββ reconcile.py # Match feeds to ledger; balance check; unrecorded list
β βββ status.py # Session-startup dashboard
β βββ tax_reserve.py # Reserve target from YTD-realized income
β βββ budget.py # Envelope check-in, runway, goals
β βββ invest.py # Dated adjustment + assertion for manual investment accounts
β βββ paystub.py # Parse payroll PDF into tax sub-amounts
β βββ home_office.py # Accountable-plan reimbursement calc (S-Corp owners)
β βββ analyze_csvs.py # One-off: import historical CSV exports
β βββ backup.py # git bundle β offsite path
βββ data/
β βββ simplefin/ # Dated JSON snapshots (committed)
β βββ <bank>/ # Dated JSON snapshots (committed)
β βββ <receivable>/ # Dated JSON snapshots (committed)
β βββ bank-exports/ # Historical CSVs used to seed the ledger
β βββ gusto/ # Paystub PDFs: paystub-YYYY-MM-DD.pdf
β βββ YYYY-mileage.csv # Business mileage log
βββ corporate/ # Formation docs, resolutions, minutes, contracts, insurance
βββ taxes/YYYY/ # Per-year tax documents, snapshot, README
βββ tests/ # pytest for common.py parsers
git init finances && cd finances
python3.12 -m venv .venv312
.venv312/bin/pip install "beancount>=3.0" "beanquery>=0.2" "fava>=1.30" "beangulp>=0.2" requests pyyaml
.venv312/bin/pip freeze > requirements.txt
.gitignore must include the venv, __pycache__, and every credential file:
.venv*/
__pycache__/
*.pyc
.simplefin_access_url # NEVER commit
.<bank>_api_key # NEVER commit
.<bank>_personal_api_key
*.swp
.DS_Store
Credential files live in the repo root, gitignored, chmod 600. Scripts read them by path; nothing reads environment variables that could leak into logs.
Invoke the venv Python directly (.venv312/bin/python scripts/foo.py) rather than source activate. Every script call in CLAUDE.md is written that way so Claude never runs the wrong interpreter.
Pre-approve the read-only and routine commands in .claude/settings.local.json so sessions do not stall on prompts:
{
"permissions": {
"allow": [
"Bash(.venv312/bin/python:*)",
"Bash(.venv312/bin/bean-check ledger/main.beancount)",
"Bash(.venv312/bin/bean-query:*)",
"Bash(git status:*)", "Bash(git add:*)", "Bash(git commit:*)",
"Bash(git diff:*)", "Bash(git log:*)",
"Bash(ls:*)", "Bash(cat:*)", "Bash(grep:*)", "Bash(wc:*)",
"Bash(pdftotext:*)"
]
}
}
Do not pre-approve anything that pushes, deletes, or transfers money. There is no money-moving automation in this system at all; the Owner executes transfers in the bank UI and Claude records them.
- One file,
ledger/main.beancount. It will reach several thousand lines within a year; that is fine. Beancount loads it in under a second. - Header:
option "title",option "operating_currency" "USD". - Account
opendirectives grouped by section with comments, all at the top. - Opening balances as a dated transaction against
Equity:OpeningBalanceson the ledger start date. Note where each number came from in a comment. - Transactions appended chronologically, under dated section comments that name the sync that produced them:
; =============================================================================
; UPDATE 2026-03-14 (SimpleFIN + <bank> sync, 9 days since 03-05)
; =============================================================================
- Each feed-sourced transaction carries
sync_idmetadata (see Part 7,reconcile.py ). Manual entries (accruals, adjustments, receivable movements) have none. - Validate with
bean-check ledger/main.beancountafter every edit. No output means valid. - Browse with
fava ledger/main.beancountwhen a human wants to look.
Adapt the names, keep the shape. The shape is what the scripts depend on: Assets:Personal:* vs Assets:Business:<Co>:*, Expenses:Personal:* vs Expenses:Business:<Co>:*, and a Payroll: subtree with tax sub-accounts.
option "title" "<Owner> Finances"
option "operating_currency" "USD"
; --- Personal cash ---
2025-12-01 open Assets:Personal:<Bank1>:Checking USD
2025-12-01 open Assets:Personal:<Bank1>:Savings USD
2025-12-01 open Assets:Personal:<Bank2>:Checking USD
2025-12-01 open Assets:Personal:<Bank2>:Savings USD ; tax bucket lives here
; --- Personal investments (manual, dated adjustments) ---
2025-12-01 open Assets:Personal:<Broker>:Portfolio USD ; taxable
2025-12-01 open Assets:Personal:<Broker>:IRA USD
2025-12-01 open Assets:Personal:<Broker>:RothIRA USD
2025-12-01 open Assets:Personal:<401kProvider>:401k USD
2025-12-01 open Assets:Personal:<HSAProvider>:HSA USD
; --- Receivables (money owed TO the owner) ---
2025-12-01 open Assets:Personal:Receivables:<Name>Loan USD
; --- Transfers in transit (multi-day transfers) ---
2025-12-01 open Assets:TransfersInTransit USD
; --- Personal liabilities ---
2025-12-01 open Liabilities:Personal:CreditCards:<Card> USD
2025-12-01 open Liabilities:Personal:Medical:<Plan> USD ; payment plans
; --- Business ---
2025-12-01 open Assets:Business:<Co>:<Bank>:Checking USD
2025-12-01 open Assets:Business:<Co>:<Bank>:Savings USD
2025-12-01 open Liabilities:Business:<Co>:<Bank>:CreditCard USD
; --- Equity ---
2025-12-01 open Equity:OpeningBalances USD
2025-12-01 open Equity:Adjustments USD ; documented corrections only
2025-12-01 open Equity:Business:<Co>:OwnerEquity USD
; --- Income ---
2025-12-01 open Income:Personal:Salary USD ; ALL wages incl. pretax deferrals (see Part 11)
2025-12-01 open Income:Personal:Interest USD
2025-12-01 open Income:Personal:Interest:<Name>Loan USD
2025-12-01 open Income:Personal:Dividends USD
2025-12-01 open Income:Personal:CapitalGains USD
2025-12-01 open Income:Personal:Investments USD ; offset for market-value adjustments (NOT taxable income)
2025-12-01 open Income:Personal:Reimbursements USD
2025-12-01 open Income:Personal:Other USD
2025-12-01 open Income:Business:<Co>:Consulting USD
2025-12-01 open Income:Business:<Co>:Cashback USD
; --- Personal expenses (fine-grained where you budget, coarse elsewhere) ---
2025-12-01 open Expenses:Personal:Housing USD ; rent/mortgage
2025-12-01 open Expenses:Personal:Housing:Utilities USD
2025-12-01 open Expenses:Personal:Utilities USD ; phone
2025-12-01 open Expenses:Personal:Food:Groceries USD
2025-12-01 open Expenses:Personal:Food:SoloMeals USD
2025-12-01 open Expenses:Personal:Food:FamilyMeals USD
2025-12-01 open Expenses:Personal:Food:Social USD
2025-12-01 open Expenses:Personal:Transportation USD
2025-12-01 open Expenses:Personal:CarMaintenance USD
2025-12-01 open Expenses:Personal:Medical USD
2025-12-01 open Expenses:Personal:Medical:Kids USD
2025-12-01 open Expenses:Personal:Activities:Kids USD
2025-12-01 open Expenses:Personal:Activities:Family USD
2025-12-01 open Expenses:Personal:Care USD
2025-12-01 open Expenses:Personal:Subscriptions USD
2025-12-01 open Expenses:Personal:Insurance USD
2025-12-01 open Expenses:Personal:Shopping USD
2025-12-01 open Expenses:Personal:Entertainment USD
2025-12-01 open Expenses:Personal:Travel USD
2025-12-01 open Expenses:Personal:Donations USD
2025-12-01 open Expenses:Personal:Taxes:Federal USD
2025-12-01 open Expenses:Personal:Taxes:State USD
2025-12-01 open Expenses:Personal:Taxes:Local USD
2025-12-01 open Expenses:Personal:Other USD
; --- Business expenses ---
2025-12-01 open Expenses:Business:<Co>:Software USD
2025-12-01 open Expenses:Business:<Co>:Equipment USD ; Section 179 hardware
2025-12-01 open Expenses:Business:<Co>:Office USD
2025-12-01 open Expenses:Business:<Co>:HomeOffice USD ; accountable-plan reimbursements
2025-12-01 open Expenses:Business:<Co>:Marketing USD
2025-12-01 open Expenses:Business:<Co>:Travel USD
2025-12-01 open Expenses:Business:<Co>:Meals USD
2025-12-01 open Expenses:Business:<Co>:Professional USD ; CPA, legal
2025-12-01 open Expenses:Business:<Co>:Insurance USD
2025-12-01 open Expenses:Business:<Co>:Benefits USD ; S-Corp owner health/HSA
2025-12-01 open Expenses:Business:<Co>:ReferralFees USD
2025-12-01 open Expenses:Business:<Co>:Payroll:Salary USD ; net pay portion of wages
2025-12-01 open Expenses:Business:<Co>:Payroll:Salary401k USD ; pretax deferral portion of wages
2025-12-01 open Expenses:Business:<Co>:Payroll:Taxes:Withholding USD ; fed + state income tax withheld
2025-12-01 open Expenses:Business:<Co>:Payroll:Taxes:FICA USD ; SS + Medicare, both halves
2025-12-01 open Expenses:Business:<Co>:Payroll:Taxes:Other USD ; FUTA, SUI, local payroll taxes
2025-12-01 open Expenses:Business:<Co>:Payroll:Garnishment USD ; if any court-ordered withholding
2025-12-01 open Expenses:Business:<Co>:Other USD
; --- Contra for accountable-plan reimbursements ---
2025-12-01 open Expenses:Personal:HomeOfficeReimbursement USD
Why the payroll tax sub-accounts are split three ways: the tax-reserve script needs income-tax withholding (a prepayment of the Owner's personal tax bill) separately from FICA (a separate tax, fully settled by payroll) and other employer taxes (a pure business cost). Lumping them makes the reserve target wrong.
After each clean reconciliation, write one assertion per feed account, dated the day after the bank balance date (Beancount evaluates assertions at the start of the day), with the bank's figure:
2026-03-15 balance Assets:Personal:<Bank1>:Checking 5,210.44 USD ; SimpleFIN balance 03-14
Rule 4 of Part 1 applies: the number is read from the feed snapshot or the bank UI. If the assertion fails, that is information; do not "fix" it by editing the assertion.
Settled vs available balances. Feeds report posted transactions. Assert the settled/current balance, not the available balance. Available subtracts pending card authorizations that have no transaction yet, and those show up as phantom drift. Snapshot scripts must store the settled figure. A residual that happens to equal a known pending hold is still not proof of a clean ledger; confirm against the live settled balance.
Investment balances are tracked as net change from statements, not as individual dividends and trades:
2026-03-01 * "<Broker>" "Portfolio balance update"
; Adjustment: 12,480.19 target - 12,301.55 ledger = +178.64
Assets:Personal:<Broker>:Portfolio 178.64 USD
Income:Personal:Investments
2026-03-02 balance Assets:Personal:<Broker>:Portfolio 12,480.19 USD
Income:Personal:Investments is explicitly not taxable income and no tax script reads it. Beancount's pad directive is avoided because it backdates the adjustment to the last pad, which breaks month-over-month queries. invest.py (Part 7) generates this block.
Actual contributions to a retirement or HSA account are separate transactions with the real cash leg, so year-to-date contribution queries can exclude the balance update narration and count only real money in.
When the two banks post a transfer on different days, split it through the transit account so each leg matches its own feed record and dates stay true:
2026-02-17 * "<Bank1>" "Transfer to <Bank2> savings - initiated"
sync_id: "ACT-β¦:TRN-β¦"
Assets:TransfersInTransit 2,000.00 USD
Assets:Personal:<Bank1>:Checking -2,000.00 USD
2026-02-18 * "<Bank2>" "Transfer from <Bank1> - received"
sync_id: "<acct-uuid>:<txn-uuid>"
Assets:Personal:<Bank2>:Savings 2,000.00 USD
Assets:TransfersInTransit -2,000.00 USD
Assets:TransfersInTransit must return to zero once both legs post. The UPDATE workflow checks it every session.
A flat list of pattern rules. Patterns are regexes matched case-insensitively against the feed's raw description (and the API's counterparty name where one exists). First match wins, so put specific patterns before general ones.
- pattern: "ANTHROPIC"
account: "Expenses:Business:<Co>:Software"
payee: "Anthropic"
- pattern: "AMAZON WEB SERVICES"
account: "Expenses:Business:<Co>:Software"
payee: "AWS"
- pattern: "GOOGLE.*WORKSPACE"
account: "Expenses:Business:<Co>:Software"
payee: "Google Workspace"
- pattern: "ALASKA AIR"
account: "Expenses:Business:<Co>:Travel"
payee: "Alaska Airlines"
flags: ["review"]
- pattern: "<CLIENT NAME AS IT APPEARS>"
account: "Income:Business:<Co>:Consulting"
payee: "<Client>"
flags: ["referral_credit"] # triggers the receivable skill
- pattern: "SAFEWAY|FRED MEYER|WINCO"
account: "Expenses:Personal:Food:Groceries"
payee: "Groceries"
- pattern: "MCDONALD|TACO BELL"
account: "Expenses:Personal:Food:SoloMeals"
payee: "Fast food"
- pattern: "<CARD ISSUER>.*AUTOPAY|AUTOMATIC PAYMENT.*THANK"
account: "_TRANSFER"
payee: "<Card>"
- pattern: "<CO NAME>|MERCURYACH"
account: "_TRANSFER"
payee: "<Co>"
- pattern: "VENMO|ZELLE"
account: "Expenses:Personal:Other"
payee: "P2P"
flags: ["review"]
Schema
| Field | Meaning |
|---|---|
pattern |
Regex, case-insensitive, matched against description and payee |
account |
Target ledger account, or the sentinel _TRANSFER |
payee |
Clean payee name to write in the ledger |
flags |
Optional list: review (Claude must ask, never auto-propose as confident),reimbursable (paid personally for the business),referral_credit (income that triggers a side effect in a receivable) |
How Claude uses it
- Claude loads
RULES.yamlwhen presenting unrecorded transactions. Matched rows are proposed with the rule's account and payee.review-flagged rows and unmatched rows are presented as questions. - A bank API's own category field (e.g. "Software & Subscriptions") is a hint only.
RULES.yamlis the source of truth for account choice. - When the Owner corrects a categorization twice for the same merchant, Claude proposes a new rule. Rules are added by editing the file in the same commit as the transactions.
- Amount-conditional rules (e.g. "coffee under $20 is SoloMeals, over $20 is Social") are expressed as a comment on the rule and applied by Claude during presentation. The YAML stays simple.
No script auto-records from rules. Categorization is advisory input to the human-in-the-loop presentation step.
BUDGET.yaml is read by budget.py, status.py, and tax_reserve.py. It holds targets, account mappings, the payroll model, the tax-reserve constants, and the runway account lists. Comments in the file carry the reasoning behind each number; the numbers themselves are the Owner's.
personal_envelopes: # variable spending, monthly targets
groceries:
target: 250
account: "Expenses:Personal:Food:Groceries"
solo_meals:
target: 175
account: "Expenses:Personal:Food:SoloMeals"
description: "Coffee, fast food, meals alone"
kids_medical:
target: 200
account: "Liabilities:Personal:Medical:<Plan>"
track_liability: true # count payments made, not expense
temporary: true
personal_fixed_costs: # predictable monthly bills
rent:
target: 1900
account: "Expenses:Personal:Housing"
phone:
target: 120
account: "Expenses:Personal:Utilities"
business_fixed_costs:
software:
target: 300
account: "Expenses:Business:<Co>:Software"
description: "Itemize the recurring tools here so drift is visible"
insurance:
target: 100
account: "Expenses:Business:<Co>:Insurance"
business_payroll: # model, not tracked from ledger (payroll is lumpy)
gross_salary: { target: 5000, description: "$60k/yr" }
employer_fica: { target: 383, description: "7.65%" }
employer_taxes: { target: 250, description: "FUTA, SUI, local" }
health_insurance: { target: 900, description: "S-Corp owner benefit" }
solo_401k: { target: 2000, flexible: true } # flexible = can in a cash crunch
hsa: { target: 1000, flexible: true }
business_tax_reserve:
bucket_account: "Assets:Personal:<Bank2>:Savings" # where the reserve cash sits
prior_year_balance: 0 # balance due from last year's return, JanβApr only
prior_year_due_date: null
quarterly_schedule: # CPA-provided safe-harbor amounts
- { quarter: Q1, due_date: "2026-04-15", federal: 1500, state: 600 }
- { quarter: Q2, due_date: "2026-06-15", federal: 1500, state: 600 }
- { quarter: Q3, due_date: "2026-09-15", federal: 1500, state: 600 }
- { quarter: Q4, due_date: "2027-01-15", federal: 1500, state: 600 }
cushion: 2000 # flat buffer for unmodeled small taxes
tax_constants: # update annually from IRS / state DOR
filing_status: hoh
qbi_rate: 0.20
federal_std_deduction: 24150
state_std_deduction: 4560
federal_brackets: # upper: null = top bracket
- { upper: 17700, rate: 0.10 }
- { upper: 67450, rate: 0.12 }
- { upper: 105700, rate: 0.22 }
- { upper: 201775, rate: 0.24 }
- { upper: 256200, rate: 0.32 }
- { upper: 640600, rate: 0.35 }
- { upper: null, rate: 0.37 }
state_brackets:
- { upper: 9100, rate: 0.0475 }
- { upper: 22800, rate: 0.0675 }
- { upper: 250000, rate: 0.0875 }
- { upper: null, rate: 0.099 }
local_surtax: # optional: a flat % above a threshold (e.g. a metro tax)
rate: 0.01
threshold_single_hoh: 125000
threshold_mfj: 200000
savings_goals:
car_maintenance:
target: 2500
deadline: null
monthly_contribution: 210
runway:
personal_accounts:
- "Assets:Personal:<Bank1>:Checking"
- "Assets:Personal:<Bank2>:Checking"
- "Assets:Personal:<Bank2>:Savings"
business_accounts:
- "Assets:Business:<Co>:<Bank>:Checking"
emergency_reserves: # liquid but excluded from runway
- "Assets:Personal:<Broker>:Portfolio"
target_months: 3
Envelope-to-actual matching uses the longest matching account prefix, so Expenses:Personal:Housing (rent) and Expenses:Personal:Housing:Utilities land in different envelopes. Keep that rule in status.py and budget.py.
Every feed writes a dated JSON snapshot to data/<feed>/YYYY-MM-DD.json, committed to git. Snapshots are the audit trail: they let you go back and find the day a discrepancy first appeared by comparing each snapshot's reported balance to the ledger as of that date.
All feeds normalize to one shape so reconcile.py is source-agnostic:
{
"sync_time": "2026-03-14T09:12:00",
"start_date": "2026-03-01",
"end_date": "2026-03-14",
"accounts": [
{
"id": "<stable account id>",
"org": "<Institution>",
"name": "<Account display name>",
"beancount_account": "Assets:Personal:<Bank>:Checking",
"balance": 5210.44,
"available_balance": 5180.44,
"balance_date": 1773500000,
"currency": "USD",
"transactions": [
{
"id": "<stable txn id>",
"sync_id": "<account id>:<txn id>",
"posted": 1773400000,
"amount": -52.18,
"description": "<raw bank text>",
"payee": "<counterparty if the API gives one>",
"memo": "",
"category": "<API category hint or null>",
"status": "sent"
}
]
}
],
"errors": []
}
balanceis thesettled balance.sync_idis<account id>:<transaction id>. Transaction ids from real feeds are stable; when a feed lacks one, fall back to<account>:<posted>:<amount>:<description[:50]>.beancount_accountis set inline by API-based feeds (from a UUIDβledger map incommon.py). SimpleFIN accounts are mapped by name substring inreconcile.py'sACCOUNT_MAP.- Pending transactions are skipped; they come back as posted with the same id later.
| Feed | Script | Notes |
|---|---|---|
| SimpleFIN Bridge | simplefin.py +sync.py |
One access URL covers many institutions. setup <token> once, thenaccounts /transactions --days N . Smart sync fetches days-since-last-sync + 7 days overlap, min 7, max 30. If data is 3+ days stale, reconnect in the bridge dashboard even when status shows normal. Exclude unreliable institutions with anEXCLUDED_ORGS set and update those manually. |
| Bank API (e.g. Mercury) | <bank>.py +<bank>_sync.py |
Wrap the official CLI or REST API. Map account UUIDs to ledger accounts in common.py . Fetch balances once per token, transactions per account since aposted_start date. Default window 90 days, 7 days overlap. Gotchas to document: empty results as a literal string, category fields that are usually null, credit cards being a separate resource from deposit accounts. |
| Spreadsheet-tracked receivable/loan | <receivable>.py |
Fetch a published-CSV URL, parse Date / Principal / Interest / Payments / Paid-by / Balance / Notes columns into rows, store current_balance . TheNotes column drives categorization and Claude reads it. |
| CSV exports | analyze_csvs.py |
For history before the ledger start date. Used once to seed opening balances and category baselines, then retired. |
| Manual | invest.py |
Brokerage, 401k, HSA balances typed in from the app or statement. |
The DataSyncer class in common.py gives every feed the same list_files(), get_latest(), load_latest(), save(data, date) behavior over its directory.
Every script documents itself via --help (argparse description from the docstring's first line, epilog from the rest). CLAUDE.md says "run --help" rather than restating usage, so the docs cannot drift from the code.
Shared module. Contains:
- Paths :
PROJECT_ROOT,LEDGER_PATH,BUDGET_PATH,RULES_PATH, one*_DIRper feed, credential file paths,BACKUP_PATH. - Account maps :
<BANK>_ACCOUNTS = { "<uuid>": {"name", "org", "ledger", "token_file"} }. - Account groupings :
CASH_EQUIVALENT_ACCOUNTS,RETIREMENT_ACCOUNTS,TAX_ADVANTAGED_ACCOUNTS(retirement + HSA). - Config s :
load_budget(),load_rules(). - Beanquery helpers :
query_beancount(sql)runspython -m beanquery <ledger> "<sql>"via subprocess;parse_beanquery_output()β{key: float};parse_beanquery_scalar()β float. - Ledger reads :
get_account_balances(),get_account_balances_at_date(date),get_spending(year, month),get_last_transaction_dates(). - Tax helpers :
get_quarterly_tax_schedule(budget)marks each quarterpaidif the ledger has a matching narration (see the estimated-tax narration convention in Part 11);get_ytd_income_tax_prepayments(year)sumsPayroll:Taxes:Withholdingplus estimated payments whose narration matches<year>.*estimatedand dated after Jan 15 (so last year's Q4 payment is excluded);get_retirement_contributions_ytd(year)sums real contributions excludingbalance updatenarrations. - Runway :
get_liquid_cash(),get_monthly_burn()(sum of envelope + fixed targets),get_runway(). - Formatting :
parse_currency("-$1,234.56"),format_currency(x). DataSynceras described in Part 6.get_backup_info()β (last backup time, is_stale > 24h).
Beanquery examples the helpers use:
SELECT account, sum(position) AS balance
WHERE account ~ 'Assets' OR account ~ 'Liabilities'
GROUP BY account ORDER BY account
SELECT sum(position) AS total
WHERE account = 'Expenses:Business:<Co>:Payroll:Taxes:Withholding' AND year = 2026
Write pytest coverage for parse_currency, parse_beanquery_output, and DataSyncer in tests/test_common.py.
- Compute the window (
--days N,--full, or smart: since last snapshot + overlap). - Fetch; enrich each transaction with
sync_id; attachbeancount_accountwhere known. - Save the dated snapshot.
- Print a report: per-account balance and transaction count, warnings, accounts with zero transactions in the window (a silent-feed tell).
The center of the system. Loads the latest snapshot from each source (--source both|simplefin|<bank>, --sync YYYY-MM-DD to pick a date) and the ledger, then produces five groups and a balance check.
Matching algorithm
ledger_by_id = {normalized sync_id β txn} for every ledger txn with sync_id metadata
(sync_id may be comma-separated; each id maps to the same txn)
ledger_without_id = ledger txns with no sync_id
used_postings = set()
for each feed transaction T:
if T.id in ledger_by_id: β MATCHED
else fuzzy-match against ledger_without_id: β FUZZY (needs sync_id backfill)
same beancount account, |amount diff| < 0.01, |date diff| β€ 1 day,
posting not already used (handles same-day duplicates)
else fuzzy-match against ALL ledger txns: β SYNC_ID_MISMATCH (feed changed its id)
else β UNRECORDED
MANUAL = ledger txns without sync_id that matched nothing (accruals, adjustments)
normalize_sync_id strips any account prefix so ACT-x:TRN-y and TRN-y compare equal, and API ids <acct>:<txn> reduce to <txn>.
Balance check (the part transaction matching cannot do):
for each feed account with a reported balance:
unexplained = bank_balance β ledger_balance β sum(amounts of UNRECORDED for this account)
ok = |unexplained| < 0.01
Printed as a table: Account / Bank / Ledger / Unrec / Unexpl, flagged accounts first. A nonzero Unexpl that persists across syncs is a real missing or wrong historical entry. A transient one is usually a pending hold that clears next sync. --strict exits non-zero if any account fails, for use as a gate.
Outputs
- Default: human report (summary counts, balance check, unrecorded table, fuzzy table with ledger line numbers, mismatches, manual list).
--json: everything, for Claude to consume programmatically.--backfill: emitssync_id: "β¦"lines with ledger line numbers for each fuzzy match, to paste under the transaction header.
Internal transfers: when a ledger entry has postings to two feed-connected accounts, both feed records fuzzy-match the same entry (different postings). Record with both sync_ids comma-separated on the one entry so future runs match exactly.
The session-startup dashboard. Sections, in order:
- Accounts β every asset and liability balance, with a staleness marker when the last transaction is > 3 days old (
7d,21d β οΈ). Net worth line. - Investment growth β cash-equivalent and tax-advantaged totals: current, month-over-month, year-to-date, using
get_account_balances_at_date. - Retirement progress β YTD contributions vs annual limits for each bucket (employee 401k, employer 401k, HSA, IRA), with deadlines. Limits are constants updated each January.
- Spending this month β by expense account.
- Budget status β envelopes and fixed costs actual vs target; over-budget items listed.
- Tax reserve β delegates to
tax_reserve.calculate_tax_reserve()and prints YTD income reads, prepayments, tax if the year ended today, the two obligations, the target, the bucket actual, and the top-up or surplus. Then the CPA quarterly schedule with paid/upcoming marks and the next due date. - Cash architecture β business checking net of its card vs a tripwire, personal checking, household cash vs a ceiling, emergency reserve. Constants at the top of the section.
- Runway β liquid cash minus the reserve target, divided by monthly burn.
- Other β receivable balance from its latest snapshot.
- System status β last sync date, last backup and whether it is stale.
--short prints one line: available cash, business cash with tripwire flag, bucket actual/target, runway months, receivable, sync date, backup status.
Answers: "If I earned and spent nothing more after today, how much cash must be set aside to cover tax, or my remaining safe-harbor commitments, whichever is higher?"
target = max(current_position, safe_harbor_floor) + prior_year_balance(JanβApr only) + cushion
current_position = YTD_tax β YTD_prepayments
YTD_tax = federal(W2 + K1 + other) + state(β¦) + local_surtax(β¦)
safe_harbor_floor = sum of unpaid CPA quarterlies
- Bracket-on-YTD : apply full annual brackets to income realized so far. Brackets fill from the bottom, so this is exactly what would be owed if income stopped today. No projection. The target grows as income is recorded, which is a feature: disciplined recording keeps it honest.
- W-2 wages are reconstructed from the business's payroll expense accounts:
Payroll:Salary+Payroll:Taxes:Withholding+ half ofPayroll:Taxes:FICA+Payroll:Garnishment.Income:Personal:Salarycannot be used because it also carries pretax deferrals and imputed benefits. - K-1 = business income β all business expenses for the year. Deferrals and owner benefits are already expenses, so no correction.
- Other taxable = receivable interest + dividends. Never
Income:Personal:Investments. - QBI deduction: 20% of positive K-1, capped at 20% of pre-QBI taxable income, federal only.
- Prepayments = withholding + estimated payments matched by narration.
- Everything configurable lives in
BUDGET.yaml. Running the script directly prints a verification dump of every input.
Document the known gaps in the docstring and in CLAUDE.md (capital gains not read, local taxes approximated, etc.) so they are fixed before they matter.
Monthly check-in: personal envelopes (budget / actual / remaining with (!) over and (*) within 10%), personal fixed costs, business fixed costs, payroll model, tax reserve. Then runway for personal and business separately against target_months, emergency reserve, and savings goals. --month YYYY-MM, --runway, --goals, --summary.
For track_liability envelopes, sum positive postings (payments made) and exclude opening-balance transactions.
invest.py <alias> <balance> [<alias> <balance> β¦] [--write]. Resolves aliases ( portfolio, ira, roth, 401k, hsa, or a full account path), reads the current ledger balance, prints an adjustment transaction dated today and a balance assertion dated tomorrow, plus a summary table. --write appends to the ledger. --list shows aliases. No-op when already at target.
Pure parser, never touches the ledger. pdftotext -layout the payroll PDF, then find labeled amounts. Employee vs employer columns are distinguished by the label's character position on the line (left column < ~60, right column β₯ 60). Fields: federal and state withholding, SS and Medicare (each side), FUTA, SUI, local payroll taxes, 401k deferral, garnishment, gross, net, and owner-benefit lines (HSA, medical, dental). Output includes the derived groups the ledger needs:
tax_pull_total = withholding + fica(both) + other # the single debit the bank shows
withholding = federal + state income tax
fica = ss_employee + ss_employer + medicare_employee + medicare_employer
other = futa + sui + local taxes (both sides)
expected_net = gross β employee_taxes β 401k β garnishment β (hsa + medical + dental)
net_matches = |expected_net β net_pay| β€ 0.02
--latest uses the newest data/gusto/paystub-*.pdf; --json for structured output. If net_matches is false, stop and investigate before recording.
For S-Corp owners, who cannot take the home-office deduction directly; the business reimburses under an accountable plan. Computes the monthly allocation by category: square-footage percentage for rent, water/sewer, renter's insurance, cleaning; documented usage percentage for internet and phone; incremental cost for electricity (bill delta since the workstation came online, if you can evidence it); actuals for supplies. Recurring bills come from the ledger as trailing-N-month averages by payee, with documented fallback constants when the ledger lacks clean data, and a Source column (ledger / fallback) so you notice when one flips. --annual for the rollup, --csv for the substantiation table. The output is the expense report; git history is the record.
Refuses to run with uncommitted changes (a git bundle only contains committed data). git bundle create <path> --all, then git bundle verify. --info shows size, mtime, validity. Point BACKUP_PATH at a synced or mounted offsite location. status.py flags backups older than 24 hours.
Skills hold per-source recording quirks: the things Claude must know to turn a feed row into a correct ledger entry. CLAUDE.md holds policy and the workflow; skills hold the mechanics. Keep them separate so CLAUDE.md stays readable.
Each skill is .claude/skills/<name>/SKILL.md with frontmatter that makes it auto-activate:
---
name: reconcile-<bank>
description: Reconcile <Bank> business or personal transactions against the beancount ledger using the <Bank> API. Use when the user says "reconcile <Bank>", during the UPDATE workflow, or when a new snapshot lands in data/<bank>/.
---
Sections to include:
- Sync β the exact commands and what they write.
- Reconcile β the command, and what each output group means for this source.
- Categorize and present β the presentation format (below) and the rule that the API's category is a hint,
RULES.yamlis truth. Never auto-record. - Record β add
sync_id: "<account id>:<txn id>"to every entry; comma-separate for paired records. - Verify β
bean-check, re-runreconcile.py, compare ledger balances to the settled balance. - Special patterns β every recurring multi-leg shape this source produces, with a ledger example for each: card cashback (posts to checking, never touches the card liability), card autopay (two feed records, one ledger entry with two sync_ids), external transfers (only one side visible in this feed; the other side arrives via another feed a day later β transit account), payroll debits (see Part 11), owner-benefit debits.
- Gotchas β API oddities, renamed accounts (UUIDs are stable; update the map), pending-status handling.
Presentation format (Claude prints this and waits):
data/<bank>/2026-03-14.json β 9 unrecorded
<Bank> Checking β’β’1234 (<Co>):
2026-03-12 <Client> $4,000.00 β Income:Business:<Co>:Consulting (<Client>)
2026-03-11 <Insurer> -$1,150.00 β Expenses:Business:<Co>:Benefits
2026-03-10 Amazon Web Services -$11.21 β Expenses:Business:<Co>:Software (AWS)
<Bank> Checking β’β’5678 (Personal):
2026-03-13 <Card> payment -$1,240.55 β Liabilities:Personal:CreditCards:<Card>
2026-03-09 UNKNOWN MERCHANT 4419 -$38.00 β ? (no rule; review)
Approve all / review individually / skip?
For a loan the Owner has made to someone else, tracked in a shared spreadsheet:
- Sync the CSV;compare the ledger receivable balance to the sheet's latest balance. Must match to the cent; any gap means an unrecorded row.
- Categorize each row by its Notes column : monthly interest accrual, cash payment, in-kind credit, adjustment.
- Patterns :
; Monthly interest accrual (income recognized here and ONLY here)
2026-03-31 * "<Borrower>" "Monthly interest accrual"
Assets:Personal:Receivables:<Name>Loan XXX.XX USD
Income:Personal:Interest:<Name>Loan -XXX.XX USD
; Cash payment (offset is wherever the wire landed)
2026-03-28 * "<Borrower>" "Loan payment - wire"
Assets:Personal:Receivables:<Name>Loan -1,000.00 USD
Assets:Personal:<Bank1>:Checking 1,000.00 USD
; In-kind credit: borrower settles a fee the business owes them by cancelling receivable
2026-03-20 * "<Borrower>" "Referral credit - 20% of <Client> invoice ($4,000)"
Assets:Personal:Receivables:<Name>Loan -800.00 USD
Expenses:Business:<Co>:ReferralFees 800.00 USD
- Rules : never reverse an accrual; every payment or credit is a two-leg paydown; interest income lives only in accruals so the year's 1099-INT matches the sheet. Credits count toward any monthly minimum, so a short wire in a month with a credit is not a shortfall.
- Reporting rules : cash-flow reports show the full client payment as cash in; spending reports exclude the paper
ReferralFeesexpense; loan reports show in-kind reductions as their own line; revenue forecasts use the full invoice.
Generalize: any "paid in kind" arrangement (barter, offsets, credits) follows the same shape. Cash never moves; two non-cash accounts move.
CLAUDE.md is Claude's operating manual for this repo. It grows over time; the sections below are the skeleton that has held up. Keep it under about 1,000 lines by moving mechanics into skills and script docstrings.
You are <Owner>'s AI bookkeeper. This project is the primary interface for all financial management.
**Terminology is fixed.** One term, one meaning:
| Term | Means |
|---|---|
| **tax bucket** | Cash held in `<bucket account>` for taxes |
| **reserve target** | The number `status.py` computes that the tax bucket should hold |
| **hard floor** | Reduced-spending operating mode |
| **all-on** | Normal operating mode |
**No live figures in this file.** Run `status.py`.
## Your Role
- Fetch and reconcile financial data using scripts
- Categorize transactions using `RULES.yaml`
- Present findings and wait for approval before recording
- Maintain the ledger with validated entries
- Research tax questions and financial strategies
**Key principle:** Never record transactions without <Owner>'s review.
## Session Startup
Run `.venv312/bin/python scripts/status.py`. (What it reports; `--short` for one line.)
### Tax Bucket (formula in one line; operational rules: top up / release / withdraw)
### Monthly Routines (trigger, procedure, ledger pattern, legal clock)
## Workflow Commands
### UPDATE ### REVIEW ### BUDGET ### PLAN ### TAX PREP
### Balance Reconciliation β the cent-accuracy guarantee (the two non-negotiable rules)
## Data Sources (table: source, purpose, command; per-source notes)
## Personal Profile (identity facts the bookkeeper needs: filing status, state, dependents,
support obligations, key contacts such as the CPA)
## Business (entity facts, formation, structure, bank, insurance; corporate/ layout;
when to create a resolution)
## Operating Mode (binary all-on / hard floor, trigger, actions, exit condition, essentials table)
### Cash Architecture (which account does which job, targets, signals)
### Cash Flow Levers (in order of preference)
## Current Clients (rate, mode, payment history, referral terms, surviving obligations)
## Receivables (terms, ledger account, source of truth, the in-kind rule)
## Investment Accounts (table of ledger paths and sources; the dated-adjustment rule)
## Special Transaction Handling (reimbursements, transit, debt payments, payroll pointer, mileage)
## Budget System (envelope tables with current vs proposed, business budget, goals, commands)
## Taxes (quarterly schedule for the year, payment policy, last return status)
## Retirement (limits and deadlines table, priority order, contribution recording pattern)
## Open Items
### Now / <this month> ### Calendar ### Tax cleanup ### Benefits admin ### Watch ### Long-term
## Scripts Reference (`--help` pointer, most-used list, bean-check, fava)
## File Structure
Open Items is the task system. Checkboxes with dates, moved to done (and later deleted) as they resolve. Claude adds items when it notices something, and reads them at session start. Do not keep a parallel task list anywhere else.
status.py. Read it all. Note staleness markers, over-budget envelopes, reserve delta, backup age.- Check the calendar and monthly routines in
CLAUDE.mdagainst today's date. If a routine is due (first session of the month, a quarterly due within two weeks), raise it before anything else. - Read Open Items β Now .
1. Sync: sync.py, <bank>_sync.py, <receivable>.py
2. Reconcile: reconcile.py (both sources)
3. Check Assets:TransfersInTransit for legs that now have their pair
4. Present unrecorded transactions with proposed categories (skill format). WAIT.
5. Record approved entries with sync_id metadata under a dated section header
6. Re-run reconcile.py. BALANCE CHECK must read N/N reconcile with no β οΈ and every Unexpl = 0.00
(reconcile.py --strict as the gate)
7. Write fresh bank-sourced balance assertions
8. bean-check ledger/main.beancount
9. git commit; backup.py if the last backup is > 24h old
Step 6 failing is not a formatting problem. Locate historical drift by comparing each snapshot's reported balance to the ledger balance as of that date; the day the gap appears brackets the bad entry.
Beanquery answers: balances, net worth, spending by category for a period, business P&L for a period, income by client. Claude writes the query, runs it through bean-query, and summarizes. Common ones belong in common.py as helpers.
budget.py for the month. Discuss overages by envelope. Proposed changes to targets are captured in a "Current vs Proposed" table in CLAUDE.md until the Owner decides to rewrite BUDGET.yaml.
Upcoming events, tax calendar, client status and receivables pipeline, cash-flow needs for the next 30β60 days. Output is a short list of dated actions, added to Open Items.
See Part 12.
- Home office reimbursement (S-Corp owners): first session of the month, look for
Expenses:Business:<Co>:HomeOfficedated this month. If absent, runhome_office.py --annual, presentlast month's amount, the Owner sends the ACH, record the 4-leg entry (Part 11). Substantiation must happen within 60 days or the whole reimbursement reclassifies as wages. - Receivable interest accrual at month-end.
- Investment balance updates near the 1st: Owner reads balances from each app, Claude runs
invest.py β¦ --write. - Court-ordered or fixed obligations that normally ride on payroll: if payroll is d, verify they were paid directly.
Each pattern below is a ledger shape Claude reproduces exactly. Keep them in the relevant skill or in CLAUDE.md and reference them by name.
2026-04-15 * "IRS" "2026 Q1 federal estimated"
Assets:Personal:<Bank2>:Savings -1,500.00 USD
Expenses:Personal:Taxes:Federal
2026-04-15 * "<State> DOR" "2026 Q1 <State> estimated"
Assets:Personal:<Bank2>:Savings -600.00 USD
Expenses:Personal:Taxes:State
get_quarterly_tax_schedule marks a quarter paid when the ledger has narration matching Q1 federal estimated / Q1 <State> estimated; get_ytd_income_tax_prepayments counts narrations matching <year>.*estimated dated after Jan 15. Prior-year balance-due payments must not match (e.g. "2025 OR-40 balance").
2026-03-17 * "<Card>" "Credit card auto-pay"
sync_id: "<checking id>:<txn A>, <card id>:<txn B>"
Assets:Personal:<Bank>:Checking -1,240.55 USD
Liabilities:Personal:CreditCards:<Card> 1,240.55 USD
If the two sides post on different days, use the transit pattern from Part 3 instead.
2026-03-31 * "<Bank>" "March cashback"
sync_id: "<checking id>:<txn>"
Assets:Business:<Co>:<Bank>:Checking 18.37 USD
Income:Business:<Co>:Cashback
A monthly payroll produces several bank debits on the pay date: net pay, one lumped tax pull, any garnishment, and (when active) a 401k transfer and an HSA transfer a few days later. Individual tax amounts never appear in the bank feed. Entries come from real bank debits; the paystub only tells you how to split them.
2026-01-30 * "Gusto" "January payroll - net pay"
sync_id: "<id>"
Assets:Business:<Co>:<Bank>:Checking -4,105.20 USD
Expenses:Business:<Co>:Payroll:Salary
2026-01-31 * "Gusto" "January payroll - net pay deposit"
sync_id: "<id>"
Assets:Personal:<Bank>:Checking 4,105.20 USD
Income:Personal:Salary
2026-01-30 * "Gusto" "January payroll - taxes"
sync_id: "<id>"
; withholding = fed 205.60 + state 95.50; FICA = 459.00 Γ 2; other = FUTA 42.00 + SUI 210.00 + local 141.20
Assets:Business:<Co>:<Bank>:Checking -1,612.30 USD
Expenses:Business:<Co>:Payroll:Taxes:Withholding 301.10 USD
Expenses:Business:<Co>:Payroll:Taxes:FICA 918.00 USD
Expenses:Business:<Co>:Payroll:Taxes:Other 393.20 USD
2026-01-30 * "Gusto" "January payroll - garnishment"
sync_id: "<id>"
Assets:Business:<Co>:<Bank>:Checking -350.00 USD
Expenses:Business:<Co>:Payroll:Garnishment
The business's compensation expense reflects full gross; the Owner's wage income reflects wages earned even though deferred.
2026-02-03 * "<401k provider>" "Solo 401k employee deferral"
sync_id: "<id>"
Expenses:Business:<Co>:Payroll:Salary401k 2,000.00 USD ; deductible comp expense
Assets:Business:<Co>:<Bank>:Checking -2,000.00 USD ; cash out
Income:Personal:Salary -2,000.00 USD ; wages earned (W-2 box 1/5)
Assets:Personal:<401kProvider>:401k 2,000.00 USD ; lands in the account
Employer contributions are 3 legs (no wage-income leg) because they are not W-2 wages:
2027-03-01 * "<401k provider>" "Solo 401k employer contribution"
Expenses:Business:<Co>:Payroll:Salary401k X,XXX.XX USD
Assets:Business:<Co>:<Bank>:Checking -X,XXX.XX USD
Assets:Personal:<401kProvider>:401k X,XXX.XX USD
get_retirement_contributions_ytd distinguishes them by narration (employer contribution).
For a >2% S-Corp shareholder, an HSA contribution or health premium paid by the company is a deductible wage expense to the company and imputed W-2 wages to the owner, washed at filing by the Schedule 1 deduction. Recording it as a plain company expense overstates K-1 and the reserve target.
2026-02-05 * "<HSA provider>" "HSA contribution via payroll S-Corp benefit"
sync_id: "<id>"
Expenses:Business:<Co>:Benefits 1,000.00 USD ; deductible wage expense
Assets:Business:<Co>:<Bank>:Checking -1,000.00 USD
Income:Personal:Salary -1,000.00 USD ; imputed W-2 wages
Assets:Personal:<HSAProvider>:HSA 1,000.00 USD
Health and dental premiums post to Expenses:Business:<Co>:Benefits against the bank or card debit; the imputed-wage side is verified through the paystub sanity check rather than recorded per premium.
Semantics of Income:Personal:Salary: all wages flowing to the Owner (net deposit + pretax deferrals + imputed benefits). It matches no single tax-form line; no script reads it. It exists for double-entry completeness.
2026-06-03 * "<Co>" "Home office accountable plan reimbursement - May 2026"
Expenses:Business:<Co>:HomeOffice 312.40 USD ; business deduction (reduces K-1)
Assets:Business:<Co>:<Bank>:Checking -312.40 USD ; business cash out
Assets:Personal:<Bank>:Checking 312.40 USD ; personal cash in
Expenses:Personal:HomeOfficeReimbursement -312.40 USD ; contra: non-taxable, net worth flat
Both bank legs carry sync_id once the transfer posts (comma-separated if both are in the same entry).
Record under Expenses:Business:<Co>:* with the personal cash leg, tag reimbursable in the narration or a #reimbursable tag, and reimburse by a businessβpersonal transfer that clears the running total. Quarterly, list them with a tag query.
Payments reduce a Liabilities: account; they are never an expense. The expense was recognized when the liability was opened (or in the opening balance).
2026-08-21 * "<Co>" "Owner distribution to personal"
sync_id: "<biz id>, <personal id>"
Assets:Business:<Co>:<Bank>:Checking -4,000.00 USD
Assets:Personal:<Bank>:Checking 4,000.00 USD
A single-owner S-Corp distribution is a non-taxable transfer, so it is asset-to-asset. Money going the other way (personal β business) is an owner contribution, same shape reversed. Anything drawn from the tax bucket for a non-tax purpose is routed back through the business as an explicit loan-to-personal entry so it cannot silently vanish.
data/YYYY-mileage.csv with date,from,to,miles,purpose. Not in the ledger. At tax time: miles Γ the IRS standard rate for the year as a business expense. If the home is the principal place of business, every business-purpose drive is deductible from mile 1.
taxes/2026/
βββ README.md # status, headlines, balances owed, pending follow-ups, source list
βββ tax-snapshot-2026.md # the working document handed to the CPA
βββ personal-2026-financial-summary.txt
βββ <co>-2026-financial-summary.txt
βββ <co>-reimbursements-2026.csv
βββ <every W-2, 1099, 1095, 1098, 5498, HSA statement, brokerage consolidated 1099>
βββ <signed e-file authorizations>
βββ <return client copies once filed>
βββ <state/local notices>
Built by Claude from ledger queries in January, refined as documents arrive. Sections:
- Income summary by source: W-2 (from paystubs / W-2), business pass-through (revenue, expenses, net, with a book-vs-tax reconciliation such as the 50% meals limitation), interest (receivable accrual total = expected 1099-INT), dividends and capital gains (brokerage), unemployment, other.
- Income summary table with totals.
- Deductions : retirement contributions by type, HSA, self-employed health insurance, home office (via accountable plan, so it is already in business expenses), mileage, donations, medical if itemizing, education credits.
- Non-deductible reminders (child support, personal legal, etc.) so nobody asks twice.
- Tax liability estimate : federal and state, using the same bracket math as
tax_reserve.py, compared to prepayments; expected refund or balance. - Open questions for the CPA : a numbered list. Every unresolved bookkeeping-vs-tax question from
CLAUDE.md β Open Items β Tax cleanupgoes here. - Document checklist with received/missing status.
- Source files .
Plain-text P&L and balance summaries generated from beanquery for the business (revenue by client, expenses by category, net) and for personal (income by source, deductible expense totals). These are what the CPA actually reads; keep them one page each.
Update README.md with headlines (AGI, total tax, refund/balance), the balances paid and when, any book-vs-tax differences to remember, and the pending follow-ups. File the client copy. Reconcile next year's CPA-provided quarterly vouchers into BUDGET.yaml β quarterly_schedule.
Pay the CPA-provided safe-harbor quarterlies even when current-year income drops. They are based on prior-year liability and cannot draw underpayment penalties. Any overpayment is a refund at filing. Relieve cash stress with operating-mode levers, never by trimming tax payments. Quarterly estimates and court-ordered obligations are in the "never cut" list of the hard-floor mode.
Reserve target = max(current position, safe-harbor floor) + cushion. Mid-year the floor usually dominates, so the target reads as "remaining quarterlies + cushion." That is correct, not a bug; the number is YTD-realized, not a full-year projection, and climbs as income is recorded.
Operational rules: top up when the delta is positive (transfer to the bucket account); release cautiously when negative; withdraw only for tax payments.
corporate/
βββ formation/ # articles, EIN letter, S-election acceptance, state registration
βββ minutes/ # annual meeting minutes (January)
βββ resolutions/ # numbered YYYY-## resolutions
βββ templates/ # resolution.md, annual-meeting.md, contract cover pages
βββ customers/<name>/ # per-customer working copies of contract sources (PDFs gitignored)
βββ <co>-accountable-plan.md
βββ <co>-insurance.md
βββ <signed agreements, NDAs, leases, notices, with counterparty and date in the filename>
Create a resolution for: opening bank accounts, significant contracts, salary changes, benefits enrollment, major purchases, adopting the accountable plan. Template:
**Date:** <date> **Resolution Number:** <YYYY-##>
## Subject
## Background
## Resolution
RESOLVED, that <action>.
FURTHER RESOLVED, that the Manager is authorized to take all actions necessary to effectuate this resolution.
**Adopted by:** <Owner>, Sole Member and Manager
A git commit by the sole member is an acceptable execution record for internal documents; say so in the document's status line.
Claude's job here: notice when a decision in conversation needs a resolution, draft it from the template, file dated documents with consistent names, and keep CLAUDE.md's business section pointing at them.
Put project and financial context in CLAUDE.md so the repo is portable. Claude's own memory directory is for harness-behavior rules only. Rules that have earned their place:
- Use the venv Python directly.
.venv312/bin/python scripts/foo.py. - Round numbers for planning, exact for the ledger.
- Track action items in
CLAUDE.md β Open Items, not in a session-only list. - Git is the archive. Delete expired content; never create archive folders.
- Commit before backup. Bundles only include committed data.
- UPDATE order is fixed: sync β receivable β reconcile β record β reconcile again β assert β commit β backup.
- Verification precision. "Transactions matched" and "balance verified to the cent" are different claims; say which one you mean.
- No heredocs in background shell commands. Write a temp file or run in the foreground.
- When recording from a winding-down account , ask whether the recurring charge should migrate.
- When the balance check flags a residual , say so first, before anything else in the message.
Commit message convention: imperative summary line, a short body naming what was recorded and any policy change, e.g. Record Mar 14 sync (11 txns), true up card assertion, add AWS rule.
Give Claude Code one phase at a time. Each phase has an acceptance check. Do not start the next until the check passes.
Phase 1 β Ledger and one feed (week 1)
- Create the repo layout, venv,
.gitignore,CLAUDE.mdwith Parts 1 and 9 skeleton, the account tree. - Pick a ledger start date (the 1st of a recent month). Enter opening balances from bank statements.
- Set up SimpleFIN (or the bank API). Write
common.py,simplefin.py,sync.py. - Write
reconcile.pywith matching + balance check. - Run UPDATE for the first month by hand with Claude presenting and the Owner approving.
- Accept when:
reconcile.py --strictpasses with every feed account atUnexpl 0.00and bank-sourced assertions are written.
Phase 2 β Rules, budget, dashboard (week 2)
- Seed
RULES.yamlfrom the first month's merchants. WriteBUDGET.yamlwith real targets. - Write
status.py,budget.py,invest.py,backup.py. - Accept when:
status.pyruns clean at session start and every section shows a sensible number; a backup bundle verifies.
Phase 3 β Second feed and skills (week 3)
- Add the bank API feed (
<bank>.py,<bank>_sync.py), merge intoreconcile.py --source both. - Write
reconcile-<bank>/SKILL.mdwith the special patterns observed so far. - Add the receivable feed and skill if there is one.
- Accept when: a full UPDATE runs end-to-end from the skill instructions alone, without the Owner explaining anything.
Phase 4 β Payroll and tax reserve (month 2)
- Write
paystub.py; record the first payroll with the split pattern; verifynet_matches. - Fill
business_tax_reserveinBUDGET.yamlfrom the CPA's vouchers. Writetax_reserve.py; wire it intostatus.py. - Adopt the accountable plan if S-Corp; write
home_office.py; run the first reimbursement. - Accept when: the reserve verification dump reconciles by hand to a spreadsheet for one month, and the bucket is topped up to target.
Phase 5 β History and tax prep (month 3)
- Import prior-year CSVs with
analyze_csvs.pyinto summary tables (not the ledger) for the return. - Build
taxes/YYYY/with the snapshot document and financial summaries. - Accept when: the CPA gets a folder and a one-page summary and asks fewer than five questions.
Ongoing
-
UPDATE every 1β2 weeks. Monthly routines on the first session of the month.
CLAUDE.mdopen items pruned every session. -
Each January: update tax constants, retirement limits, mileage rate, quarterly schedule; hold the annual meeting; write minutes.
-
"Run session startup." β
status.py, routines check, open items. -
"UPDATE." β the full loop.
-
"Reconcile only."
-
"What did we spend on food in August, by envelope?"
-
"Business P&L for Q2."
-
"Draft the tax snapshot for 2026 from the ledger."
-
"Prepare last month's home office reimbursement."
-
"Add a rule for β ."
-
"Something is off in ; find the drift." β snapshot-by-snapshot comparison.
| Term | Meaning |
|---|---|
| Feed | Any scripted source of bank data (SimpleFIN, bank API, CSV, spreadsheet) |
| Snapshot | A dated JSON file in data/<feed>/ holding one fetch |
sync_id |
Ledger metadata linking an entry to a feed transaction: <account id>:<txn id> |
| Unrecorded | Feed transaction with no ledger match; the session's work |
| Fuzzy match | Ledger entry matched by account + amount + date Β±1 day, missing its sync_id |
| Unexpl | bank β ledger β unrecorded; must be 0.00 |
| Transit | Assets:TransfersInTransit , holding one leg of a multi-day transfer |
| Balance update | A dated adjustment to a manually tracked investment account; not income |
| Tax bucket | The savings account holding the reserve |
| Reserve target | max(current position, safe-harbor floor) + cushion |
| Safe-harbor floor | Remaining unpaid CPA quarterlies |
| Current position | Tax on YTD-realized income minus YTD prepayments |
| Envelope | A monthly variable-spending target mapped to an account prefix |
| Hard floor / all-on | The two operating modes; nothing in between |
| Accountable plan | IRS framework letting the business reimburse the owner tax-free with substantiation within 60 days |
| 4-leg entry | A transaction that moves cash and simultaneously records the business expense and the owner's income/contra so tax reads stay correct |