Skip to content
Ledger CLI: Plain Text Accounting

Ledger CLI: Plain Text Accounting

Published: at 05:18 PM

Table of contents

Open Table of contents

Introduction

Every transaction I make - every bus ride, every coffee, every gift - goes into one plain text file. No banking app, no aggregator, no spreadsheet. Just a text file and Ledger, a command-line double-entry accounting tool.

If presentations can be code, accounting definitely can.

Note: All names, banks, tickers and amounts in this post are made up. The structure is exactly how I run mine; the numbers are not.

Purpose

  1. Understand plain text accounting - double-entry in one minute, and why a text file beats an app.
  2. Structure a journal - the account tree, multi-currency handling (USD, EUR, gold, silver, stocks, crypto) and the file conventions I settled on.
  3. Query it - the aliases and commands I actually run, including a few that answer suspiciously specific questions.

Prerequisites

Why a Text File and Not an App

And it is free, works offline, and the file will still open in 30 years.

Double-Entry in One Minute

Every transaction moves value between at least two accounts, and the postings must sum to zero. All accounts live under five roots. The comment header at the top of my file is the whole theory:

; Assets - Bank Accounts, Wallet, Investments, Loans credited
; Income - Paychecks, Gifts received, Dividends, Interest
; Expense - Groceries, Taxes, Gifts given, Donations
; Liabilities - Mortgage, Credit cards, Student loans, Loans owed
; Equity - Untrackable errors or Opening Balances

One sign convention to internalize: Income is negative. Money flows from Income into Assets, so a salary posting is -3000 USD on the Income side. It feels wrong for a day and then it clicks.

The Journal

A routine day in the file looks like this:

2026-07-10
	Assets:Cash:Bank:WeeWeeBank:Checking
	Expenses:Groceries:PurPurMarket			7.12 USD
	Expenses:Transport:Transit:Bus			0.60 USD
	Expenses:Food:Restaurants:Doner			9.80 USD

The conventions I converged on:

A payday - income negative in the wild, with the day’s expenses and cashback netted into the same transaction:

2026-07-02 Pay Day
	Assets:Cash:Bank:WeeWeeBank:Checking
	Income:Salary:Employer				-3000.00 USD
	Expenses:Groceries:PurPurMarket			17.61 USD
	Income:Cashback:Bank:WeeWeeBank			-5.00 USD

Debts are assets. Lending moves money from the bank into Assets:Receivable:<Person>, repayment moves it back, and if it is never coming back - reclassify and accept reality:

2026-03-01
	Assets:Receivable:Friend			100 USD
	Assets:Cash:Bank:WeeWeeBank:Checking

2026-04-01
	Assets:Cash:Bank:WeeWeeBank:Checking		100 USD
	Assets:Receivable:Friend

2026-06-30 Clearing Receivables
	Assets:Receivable:Friend			-50 USD
	Expenses:Gifts:Monetary:Friend			50 USD

The receivable balance is always the honest answer to “who owes me what”.

More Than One Currency

In Ledger everything is a commodity: USD is one, EUR is one, a gram of gold is one, a share of ACME is one. Buying an asset records the total cost with @@:

2026-01-05 Buy gold and silver
	Assets:Cash:Bank:WeeWeeBank:Checking
	Assets:Investments:PreciousMetals:Gold		5 GOLD @@ 545 USD
	Assets:Investments:PreciousMetals:Silver	20 SILVER @@ 26 USD
	Expenses:Fees:MetalPremium			21 USD

Note the premium split into its own expense - the bank’s margin over spot is a fee, not an investment. Stocks and crypto work the same way:

2026-01-06
	Assets:Cash:Bank:WeeWeeBank:Checking
	Assets:Investments:Stocks:Broker		2 ACME @@ 505 USD
	Assets:Crypto:Exchange				0.5 COIN @@ 60 USD

Even exchanging cash is just a commodity purchase:

2026-01-10
	Assets:Cash:Pocket:Checking			100 EUR @@ 108 USD
	Assets:Cash:Bank:WeeWeeBank:Checking

Market prices are P directives that I update by hand every month or two:

P 2026-07-01 EUR 1.08 USD
P 2026-07-01 GOLD 109 USD
P 2026-07-01 SILVER 1.30 USD
P 2026-07-01 ACME 260 USD

Plain ledger bal Assets then reports each commodity separately; add -X USD and the whole portfolio collapses into one USD number at the latest known prices.

Where Did the Cash Go

Pocket cash never reconciles. Instead of pretending it does, I book the difference honestly:

	Expenses:FX:Loss				2.05 USD ; idk where

Those entries accumulate into the real, measured cost of carrying physical cash. Income:FX:Gain exists too, for the rarer opposite case of finding money on your own shelf.

Daily Workflow

Aliases, because I run these constantly:

alias lg='ledger'
alias lgb='ledger b'
alias lgba='ledger b Assets'
alias lgbe='ledger b Expenses'
alias lgbi='ledger b Income'

At the end of the day I append one transaction from the bank app history and one from memory for cash. To lower the friction I pre-create empty date lines for the week ahead:

2026-07-25
2026-07-26
2026-07-27

They parse fine, and an empty date staring at you is a surprisingly effective nag to fill it.

The Queries I Actually Run

# net worth, per commodity
lgba -f finances.ledger

# net worth, one number
lgba -f finances.ledger -X USD

# liquid money only - boolean filters on account names
lgba -f finances.ledger -X USD and not gold and not crypto and not saving

# this month's burn and income
lgbe -f finances.ledger --begin "2026-07-01"
lgbi -f finances.ledger --begin "2026-07-01"

# monthly expense totals across the whole history
lg -f finances.ledger reg ^Expenses -X USD -M --collapse

# lifetime damage in one supermarket chain
lgb -f finances.ledger PurPurMarket

The query language is just account regexes plus and/or/not plus date windows. That is enough to answer almost anything.

What the File Knows About You

After a while, the fun part is asking questions you never planned to ask:

# most frequent expense accounts - where does life actually happen
lg -f finances.ledger reg ^Expenses -F "%(account)\n" | sort | uniq -c | sort -rn | head

# how many distinct restaurants have I tried
lg -f finances.ledger accounts Expenses:Food:Restaurants | wc -l

# savings rate in one line
lg -f finances.ledger bal ^Income ^Expenses -X USD --depth 1

# average cost of a taxi ride vs a bus ride - you already know, but now in numbers
lg -f finances.ledger bal Expenses:Transport:Taxi -X USD
lg -f finances.ledger reg Expenses:Transport:Taxi | wc -l

I will keep my own numbers to myself, but I promise some of them surprised me.

Tips

1. LLM

Once an LLM has the Ledger manual and your file, it writes any query you can describe in words, finds patterns you did not think to look for, and catches inconsistencies. This is the same trick as with Presenterm: plain text formats and LLMs are a natural pair.

2. Guard the Account Names

A typo silently creates a new account - Expenses:Grocery next to Expenses:Groceries will split your data without a single error. Run ledger accounts once in a while and read the list; renames are one sed away.

3. Start Stupid, Split Later

Do not design the perfect taxonomy upfront. Log everything to Expenses:Food and split into subaccounts the day you have a question that needs the split. The file is text; restructuring it is editing, not migration.

4. Edit in Neovim

The vim-ledger plugin adds syntax highlighting, folding, account completion and amount alignment for .ledger files, which makes a growing journal far easier to read and keep consistent.

Conclusion

One text file under git, two minutes a day. In return, any question about my money is one command away - in a format no vendor can take away or discontinue.

References

Appendix: A Sample Journal

Everything from this post in one small, valid file. Save it as finances.ledger and start querying:

; Assets - Bank Accounts, Wallet, Investments, Loans credited
; Income - Paychecks, Gifts received, Dividends, Interest
; Expense - Groceries, Taxes, Gifts given, Donations
; Liabilities - Mortgage, Credit cards, Student loans, Loans owed
; Equity - Untrackable errors or Opening Balances

P 2026-01-01 EUR 1.08 USD
P 2026-01-01 GOLD 105 USD
P 2026-01-01 SILVER 1.20 USD
P 2026-01-01 ACME 250 USD

2026-01-01 Opening Balances
	Assets:Cash:Bank:WeeWeeBank:Checking		1500 USD
	Assets:Cash:Bank:WeeWeeBank:Saving		2000 USD
	Assets:Cash:Pocket:Checking			120 USD
	Assets:Cash:Pocket:Checking			50 EUR
	Liabilities:StudentLoans:University		-4500 USD
	Equity:OpeningBalances

2026-01-02 Pay Day
	Assets:Cash:Bank:WeeWeeBank:Checking
	Income:Salary:Employer				-3000.00 USD
	Expenses:Groceries:PurPurMarket			17.61 USD
	Income:Cashback:Bank:WeeWeeBank			-5.00 USD

2026-01-03
	Assets:Cash:Bank:WeeWeeBank:Checking
	Expenses:Food:Cafes:Coffee			3.20 USD
	Expenses:Transport:Transit:Bus			0.60 USD
	Expenses:Transport:Transit:Bus			0.60 USD
	Expenses:Subscriptions:Music			11.99 USD

2026-01-04
	Assets:Cash:Pocket:Checking
	Expenses:Food:Restaurants:Doner			9.80 USD
	Expenses:FX:Loss				2.05 USD ; idk where

2026-01-05 Buy gold and silver
	Assets:Cash:Bank:WeeWeeBank:Checking
	Assets:Investments:PreciousMetals:Gold		5 GOLD @@ 545 USD
	Assets:Investments:PreciousMetals:Silver	20 SILVER @@ 26 USD
	Expenses:Fees:MetalPremium			21 USD

2026-01-06
	Assets:Cash:Bank:WeeWeeBank:Checking
	Assets:Investments:Stocks:Broker		2 ACME @@ 505 USD
	Assets:Crypto:Exchange				0.5 COIN @@ 60 USD

2026-01-07
	Assets:Receivable:Friend			100 USD
	Assets:Cash:Bank:WeeWeeBank:Checking

2026-01-10
	Assets:Cash:Pocket:Checking			100 EUR @@ 108 USD
	Assets:Cash:Bank:WeeWeeBank:Checking

2026-01-15
	Assets:Cash:Bank:WeeWeeBank:Checking		100 USD
	Assets:Receivable:Friend

2026-01-16
2026-01-17
2026-01-18