cd /news/developer-tools/claude-code-bookkeeper-playbook Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-125238] src=gist.github.com β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

Claude Code Bookkeeper Playbook

Developer Peat Bakke has published a detailed playbook for building a personal and small-business bookkeeping system operated by Claude Code, Anthropic's AI coding assistant. The system, which has run his household and consulting company for nearly a year, uses a plain-text double-entry ledger, scripted bank feeds, and a reconciliation loop that proves cent-level accuracy. Bakke emphasizes that Claude proposes transactions but the owner approves them, and that balance assertions must come from the bank, not the ledger, to avoid hidden errors.

by read42 min views1 publishedSep 9, 2026

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.md in 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 Business subtree 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.

  1. 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.
  2. 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.
  3. 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.
  4. Balance assertions come from the bank, never from the ledger. Copying the ledger's own number into abalance directive certifies your own error. A dropped grocery charge hid for three months this way before the rule was made absolute.
  5. Residuals get investigated, never plugged. When bank and ledger disagree, find the real missing or wrong entry. NoEquity:Adjustments plugs to make a number go away.
  6. No live figures in CLAUDE.md. Balances, year-to-date totals, and receivables are derived.CLAUDE.md holds procedure, policy, profile facts, and open items. A script (status.py ) reports the numbers.
  7. Fixed terminology. One term, one meaning. Define the handful of house terms in a table at the top ofCLAUDE.md and never introduce synonyms. Drift in vocabulary becomes drift in bookkeeping.
  8. Git is the archive. Delete expired content outright. Nodocs/archive/ , no commented-out history.git log recovers anything.
  9. Round numbers for planning, exact numbers for the ledger. Budget and strategy discussions use round figures; ledger entries are exact to the cent.
  10. 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 open directives grouped by section with comments, all at the top.
  • Opening balances as a dated transaction against Equity:OpeningBalances on 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_id metadata (see Part 7,reconcile.py ). Manual entries (accruals, adjustments, receivable movements) have none.
  • Validate with bean-check ledger/main.beancount after every edit. No output means valid.
  • Browse with fava ledger/main.beancount when 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.yaml when 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.yaml is 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": []
}
  • balance is thesettled balance.
  • sync_id is<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_account is 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*_DIR per 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 quarterpaid if the ledger has a matching narration (see the estimated-tax narration convention in Part 11);get_ytd_income_tax_prepayments(year) sumsPayroll:Taxes:Withholding plus estimated payments whose narration matches<year>.*estimated and dated after Jan 15 (so last year's Q4 payment is excluded);get_retirement_contributions_ytd(year) sums real contributions excludingbalance update narrations.
  • Runway :get_liquid_cash() ,get_monthly_burn() (sum of envelope + fixed targets),get_runway() .
  • Formatting :parse_currency("-$1,234.56") ,format_currency(x) .
  • DataSyncer as 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.

  1. Compute the window (--days N ,--full , or smart: since last snapshot + overlap).
  2. Fetch; enrich each transaction with sync_id ; attachbeancount_account where known.
  3. Save the dated snapshot.
  4. 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:

  1. Accounts β€” every asset and liability balance, with a staleness marker when the last transaction is > 3 days old (7d ,21d ⚠️ ). Net worth line.
  2. Investment growth β€” cash-equivalent and tax-advantaged totals: current, month-over-month, year-to-date, usingget_account_balances_at_date .
  3. Retirement progress β€” YTD contributions vs annual limits for each bucket (employee 401k, employer 401k, HSA, IRA), with deadlines. Limits are constants updated each January.
  4. Spending this month β€” by expense account.
  5. Budget status β€” envelopes and fixed costs actual vs target; over-budget items listed.
  6. Tax reserve β€” delegates totax_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.
  7. 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.
  8. Runway β€” liquid cash minus the reserve target, divided by monthly burn.
  9. Other β€” receivable balance from its latest snapshot.
  10. 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:Salary cannot 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. NeverIncome: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:

  1. Sync β€” the exact commands and what they write.
  2. Reconcile β€” the command, and what each output group means for this source.
  3. Categorize and present β€” the presentation format (below) and the rule that the API's category is a hint,RULES.yaml is truth. Never auto-record.
  4. Record β€” addsync_id: "<account id>:<txn id>" to every entry; comma-separate for paired records.
  5. Verify β€”bean-check , re-runreconcile.py , compare ledger balances to the settled balance.
  6. 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.
  7. 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:

  1. 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.
  2. Categorize each row by its Notes column : monthly interest accrual, cash payment, in-kind credit, adjustment.
  3. 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
  1. 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.
  2. Reporting rules : cash-flow reports show the full client payment as cash in; spending reports exclude the paperReferralFees expense; 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.

  1. status.py . Read it all. Note staleness markers, over-budget envelopes, reserve delta, backup age.
  2. Check the calendar and monthly routines in CLAUDE.md against today's date. If a routine is due (first session of the month, a quarterly due within two weeks), raise it before anything else.
  3. 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 forExpenses:Business:<Co>:HomeOffice dated 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 runsinvest.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:

  1. 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.
  2. Income summary table with totals.
  3. 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.
  4. Non-deductible reminders (child support, personal legal, etc.) so nobody asks twice.
  5. Tax liability estimate : federal and state, using the same bracket math astax_reserve.py , compared to prepayments; expected refund or balance.
  6. Open questions for the CPA : a numbered list. Every unresolved bookkeeping-vs-tax question fromCLAUDE.md β†’ Open Items β†’ Tax cleanup goes here.
  7. Document checklist with received/missing status.
  8. 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.md with 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.py with matching + balance check.
  • Run UPDATE for the first month by hand with Claude presenting and the Owner approving.
  • Accept when:reconcile.py --strict passes with every feed account atUnexpl 0.00 and bank-sourced assertions are written.

Phase 2 β€” Rules, budget, dashboard (week 2)

  • Seed RULES.yaml from the first month's merchants. WriteBUDGET.yaml with real targets.
  • Write status.py ,budget.py ,invest.py ,backup.py .
  • Accept when:status.py runs 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.md with 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_reserve inBUDGET.yaml from 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.py into 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.md open 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
── more in #developer-tools 4 stories Β· sorted by recency
── more on @peat bakke 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/claude-code-bookkeep…] indexed:0 read:42min 2026-09-09 Β· β€”