# Claude Code Bookkeeper Playbook

> Source: <https://gist.github.com/peat/70360a13f09d7ef03cac873561ae6f88>
> Published: 2026-09-09 19:03:54+00:00

**From Peat Bakke** · [peat@peat.org](mailto:peat@peat.org) · Bluesky [@peat.org](https://bsky.app/profile/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](https://beancount.github.io/) v3 (`beancount` ,`beanquery` ,`fava` ).
- At least one bank feed: a [SimpleFIN Bridge](https://beta-bridge.simplefin.org/) 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 are*inputs* 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 is*missing* . Both run every session.
4. **Balance assertions come from the bank, never from the ledger.** Copying the ledger's own number into a`balance` 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. No`Equity: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 of`CLAUDE.md` and never introduce synonyms. Drift in vocabulary becomes drift in bookkeeping.
8. **Git is the archive.** Delete expired content outright. No`docs/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.

```
# Transaction categorization rules
# Patterns matched case-insensitively. Flags: review, reimbursable, referral_credit

# --- Business software ---
- 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"

# --- Business travel (could be personal → always review) ---
- pattern: "ALASKA AIR"
  account: "Expenses:Business:<Co>:Travel"
  payee: "Alaska Airlines"
  flags: ["review"]

# --- Client income ---
- pattern: "<CLIENT NAME AS IT APPEARS>"
  account: "Income:Business:<Co>:Consulting"
  payee: "<Client>"
  flags: ["referral_credit"]      # triggers the receivable skill

# --- Groceries ---
- pattern: "SAFEWAY|FRED MEYER|WINCO"
  account: "Expenses:Personal:Food:Groceries"
  payee: "Groceries"

# --- Small solo meals; if > $20 use Social instead ---
- pattern: "MCDONALD|TACO BELL"
  account: "Expenses:Personal:Food:SoloMeals"
  payee: "Fast food"

# --- Internal transfers: sentinel account, handled specially ---
- pattern: "<CARD ISSUER>.*AUTOPAY|AUTOMATIC PAYMENT.*THANK"
  account: "_TRANSFER"
  payee: "<Card>"
- pattern: "<CO NAME>|MERCURYACH"
  account: "_TRANSFER"
  payee: "<Co>"

# --- Catch-all, always review ---
- 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 pause 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 the**settled** 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 in`common.py` ). SimpleFIN accounts are mapped by name substring in`reconcile.py` 's`ACCOUNT_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, then`accounts` /`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 an`EXCLUDED_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 a`posted_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` . The**Notes 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 loaders** :`load_budget()` ,`load_rules()` .
- **Beanquery helpers** :`query_beancount(sql)` runs`python -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 quarter`paid` if the ledger has a matching narration (see the estimated-tax narration convention in Part 11);`get_ytd_income_tax_prepayments(year)` sums`Payroll: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 excluding`balance 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` ; attach`beancount_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` : emits`sync_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, using`get_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 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.
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 of`Payroll: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. 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:

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** — add`sync_id: "<account id>:<txn id>"` to every entry; comma-separate for paired records.
5. **Verify** —`bean-check` , re-run`reconcile.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 paper`ReferralFees` 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.

```
# Instructions for Claude Code

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 for`Expenses:Business:<Co>:HomeOffice` dated this month. If absent, run`home_office.py --annual` , present*last* 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 paused, 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 as`tax_reserve.py` , compared to prepayments; expected refund or balance.
6. **Open questions for the CPA** : a numbered list. Every unresolved bookkeeping-vs-tax question from`CLAUDE.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:

```
# Resolution of <Co>, LLC
**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 at`Unexpl 0.00` and bank-sourced assertions are written.

**Phase 2 — Rules, budget, dashboard (week 2)**

- Seed `RULES.yaml` from the first month's merchants. Write`BUDGET.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 into`reconcile.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; verify`net_matches` .
- Fill `business_tax_reserve` in`BUDGET.yaml` from the CPA's vouchers. Write`tax_reserve.py` ; wire it into`status.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 |
