Over the past six months, I’ve tracked my money with hledger
—a plain text
double-entry accounting system written in Haskell. It’s been
surprisingly painless.
My previous attempts to pick up real accounting tools floundered. Hosted tools are privacy nightmares, and my stint with GnuCash didn’t last.
But after stumbling on Dmitry Astapov’s “Full-fledged
hledger
” wiki1, it
clicked—eventually consistent accounting. Instead of
modeling your money all at once, take it one hacking session at a
time.
It should be easy to work towards eventual consistency. […] I should be able to [add financial records] bit by little bit, leaving things half-done, and picking them up later with little (mental) effort.
– Dmitry Astapov, Full-Fledged Hledger
Principles of my system
I’ve cobbled together a system based on these principles:
- Avoid manual entry – Avoid typing in each transaction. Instead, rely on CSVs from the bank.
- CSVs as truth – CSVs are the only things that matter. Everything else can be blown away and rebuilt anytime.
- Embrace version control – Keep everything under version control in Git for easy comparison and safe experimentation.
Learn hledger
in five minutes
hledger
concepts are heady, but its use is simple. I
divide the core concepts into two categories:
- Stuff
hledger
cares about:- Transactions – how
hledger
moves money between accounts. - Journal files – files full of transactions
- Transactions – how
- Stuff I care about:
- Rules files – how I set up accounts, import CSVs, and move money between accounts.
- Reports – help me see where my money is going and if I messed up my rules.
Transactions move money between accounts:
2024-01-01 Payday
income:work $-100.00
assets:checking $100.00
This transaction shows that on Jan 1, 2024, money moved from
income:work
into assets:checking
—Payday.
The sum of each transaction should be $0. Money comes from somewhere, and the same amount goes somewhere else—double-entry accounting. This is powerful technology—it makes mistakes impossible to ignore.
Journal files are text files containing one or more transactions:
2024-01-01 Payday
income:work $-100.00
assets:checking $100.00
2024-01-02 QUANSHENG UVK5
assets:checking $-29.34
expenses:fun:radio $29.34
Rules files transform CSVs into journal files via regex matching.
Here’s a CSV from my bank:
Transaction Date,Description,Category,Type,Amount,Memo
09/01/2024,DEPOSIT Paycheck,Payment,Payment,1000.00,
09/04/2024,PizzaPals Pizza,Food & Drink,Sale,-42.31,
09/03/2024,Amazon.com*XXXXXXXXY,Shopping,Sale,-35.56,
09/03/2024,OBSIDIAN.MD,Shopping,Sale,-10.00,
09/02/2024,Amazon web services,Personal,Sale,-17.89,
And here’s a checking.rules
to transform that CSV into a
journal file so I can use it with hledger
:
# checking.rules
# --------------
# Map CSV fields → hledger fields[0]
fields date,description,category,type,amount,memo,_
# `account1`: the account for the whole CSV.[1]
account1 assets:checking
account2 expenses:unknown
skip 1
date-format %m/%d/%Y
currency $
if %type Payment
account2 income:unknown
if %category Food & Drink
account2 expenses:food:dining
# [0]: <https://hledger.org/hledger.html#field-names>
# [1]: <https://hledger.org/hledger.html#account-field>
With these two files (checking.rules
and
2024-09_checking.csv
), I can make the CSV into a
journal:
$ > 2024-09_checking.journal \
hledger print \
--rules-file checking.rules \
-f 2024-09_checking.csv
$ head 2024-09_checking.journal
2024-09-01 DEPOSIT Paycheck
assets:checking $1000.00
income:unknown $-1000.00
2024-09-02 Amazon web services
assets:checking $-17.89
expenses:unknown $17.89
Reports are interesting ways to view transactions between accounts.
There are registers, balance sheets, and income statements:
$ hledger incomestatement \
--depth=2 \
--file=2024-09_bank.journal
Revenues:
$1000.00 income:unknown
-----------------------
$1000.00
Expenses:
$42.31 expenses:food
$63.45 expenses:unknown
-----------------------
$105.76
-----------------------
Net: $894.24
At the beginning of September, I spent $105.76
and made
$1000
, leaving me with $894.24
.
But a good chunk is going to the default expense account,
expenses:unknown
. I can use the
hleger aregister
to see what those transactions are:
$ hledger areg expenses:unknown \
--file=2024-09_checking.journal \
-O csv | \
csvcut -c description,change | \
csvlook
| description | change |
| ------------------------ | ------ |
| OBSIDIAN.MD | 10.00 |
| Amazon web services | 17.89 |
| Amazon.com*XXXXXXXXY | 35.56 |
l
Then, I can add some more rules to my
checking.rules
:
if OBSIDIAN.MD
account2 expenses:personal:subscriptions
if Amazon web services
account2 expenses:personal:web:hosting
if Amazon.com
account2 expenses:personal:shopping:amazon
Now, I can reprocess my data to get a better picture of my spending:
$ > 2024-09_bank.journal \
hledger print \
--rules-file bank.rules \
-f 2024-09_bank.csv
$ hledger bal expenses \
--depth=3 \
--percent \
-f 2024-09_checking2.journal
30.0 % expenses:food:dining
33.6 % expenses:personal:shopping
9.5 % expenses:personal:subscriptions
16.9 % expenses:personal:web
--------------------
100.0 %
For the Amazon.com purchase, I lumped it into the
expenses:personal:shopping
account. But I could dig
deeper—download my
order history from Amazon and categorize that spending.
This is the power of working bit-by-bit—the data guides you to the next, deeper rabbit hole.
Goals and non-goals
Why am I doing this? For years, I maintained a monthly spreadsheet of account balances. I had a balance sheet. But I still had questions.
Before diving into accounting software, these were my goals:
- Granular understanding of my spending – The big one. This is where my monthly spreadsheet fell short. I knew I had money in the bank—I kept my monthly balance sheet. I budgeted up-front the % of my income I was saving. But I had no idea where my other money was going.
- Data privacy – I’m unwilling to hand the keys to my accounts to YNAB or Mint.
- Increased value over time – The more time I put in, the more value I want to get out—this is what you get from professional tools built for nerds. While I wished for low-effort setup, I wanted the tool to be able to grow to more uses over time.
Non-goals—these are the parts I never cared about:
- Investment tracking – For now, I left this out of scope. Between monthly balances in my spreadsheet and online investing tools’ ability to drill down, I was fine.2
- Taxes – Folks smarter than me help me understand my yearly taxes.3
- Shared system – I may want to share reports from this system, but no one will have to work in it except me.
- Cash – Cash transactions are unimportant to me. I withdraw money from the ATM sometimes. It evaporates.
hledger
can track all these things. My setup is flexible
enough to support them someday. But that’s unimportant to me right
now.
Monthly maintenance
I spend about an hour a month checking in on my money Which frees me to spend time making fancy charts—an activity I perversely enjoy.
Here’s my setup:
$ tree ~/Documents/ledger
.
├── export
│ ├── 2024-balance-sheet.txt
│ └── 2024-income-statement.txt
├── import
│ ├── in
│ │ ├── amazon
│ │ │ └── order-history.csv
│ │ ├── credit
│ │ │ ├── 2024-01-01_2024-02-01.csv
│ │ │ ├── ...
│ │ │ └── 2024-10-01_2024-11-01.csv
│ │ └── debit
│ │ ├── 2024-01-01_2024-02-01.csv
│ │ ├── ...
│ │ └── 2024-10-01_2024-11-01.csv
│ └── journal
│ ├── amazon
│ │ └── order-history.journal
│ ├── credit
│ │ ├── 2024-01-01_2024-02-01.journal
│ │ ├── ...
│ │ └── 2024-10-01_2024-11-01.journal
│ └── debit
│ ├── 2024-01-01_2024-02-01.journal
│ ├── ...
│ └── 2024-10-01_2024-11-01.journal
├── rules
│ ├── amazon
│ │ └── journal.rules
│ ├── credit
│ │ └── journal.rules
│ ├── debit
│ │ └── journal.rules
│ └── common.rules
├── 2024.journal
├── Makefile
└── README
Process:
- Import – download a CSV for the month from each
account and plop it into
import/in/<account>/<dates>.csv
- Make – run
make
- Squint – Look at
git diff
; if it looks good,git add . && git commit -m "💸"
otherwise reviewhledger areg
to see details.
The Makefile
generates everything under
import/journal
:
- journal files from my CSVs using their corresponding rules.
- reports in the
export
folder
I include all the journal files in the 2024.journal
with
the line: include ./import/journal/*/*.journal
Here’s the Makefile
:
SHELL := /bin/bash
RAW_CSV = $(wildcard import/in/**/*.csv)
JOURNALS = $(foreach file,$(RAW_CSV),$(subst /in/,/journal/,$(patsubst %.csv,%.journal,$(file))))
.PHONY: all
all: $(JOURNALS)
hledger is -f 2024.journal > export/2024-income-statement.txt
hledger bs -f 2024.journal > export/2024-balance-sheet.txt
.PHONY clean
clean:
rm -rf import/journal/**/*.journal
import/journal/%.journal: import/in/%.csv
@echo "Processing csv $< to $@"
@echo "---"
@mkdir -p $(shell dirname $@)
@hledger print --rules-file rules/$(shell basename $$(dirname $<))/journal.rules -f "$<" > "$@"
If I find anything amiss (e.g., if my balances are different than
what the bank tells me), I look at hleger areg
. I may tweak
my rules or my CSVs and then I run
make clean && make
and try again.
Simple, plain text accounting made simple.
And if I ever want to dig deeper, hledger
’s docs have more to
teach. But for now, the balance of effort vs. reward is perfect.
while reading a blog post from Jonathan Dowland↩︎
Note, this is covered by full-fledged hledger – Investements↩︎
Also covered in full-fledged hledger – Tax returns↩︎