find-subscriptions
Scans bank and credit card CSV exports to surface recurring charges — subscriptions, memberships, and other repeat payments you may have forgotten about.
Requirements
- Ruby 3.x
Installation
gem install find-subscriptions
Or clone the repo and run directly:
git clone https://github.com/jeffreybaird/find-subscriptions
cd find-subscriptions
bundle install
./bin/find-subscriptions --files EXPORT.csv
Usage
find-subscriptions --files EXPORT.csv []
Run with no arguments (or --help) to see the full help menu:
find-subscriptions
find-subscriptions --help
Get detailed help for a specific flag:
find-subscriptions --sort --help
find-subscriptions --format --help
find-subscriptions --set-config --help
Options
| Flag | Description |
|---|---|
--files FILES |
Comma-separated list of CSV files to scan |
--schema NAME |
Force a schema instead of auto-detecting from headers |
--known-payees PATH |
Path to a known-payees YAML file; matching payees are filtered out of results |
--sort ORDER |
Sort order for results (see below) |
--inactive-for DURATION |
Hide subscriptions with no recent transactions (see below) |
--min-amount AMOUNT |
Hide subscriptions with a recurring charge below AMOUNT (e.g. 5.00) |
--from DATE |
Only include transactions on or after DATE (YYYY-MM-DD) |
--to DATE |
Only include transactions on or before DATE (YYYY-MM-DD) |
--format FORMAT |
Output format: text (default), json, csv |
--set-config PATH |
Register a config YAML file to use as defaults on future runs |
Output formats
| Value | Output |
|---|---|
text |
Human-readable table (default) |
json |
Pretty-printed JSON array — pipe into jq or save for further processing |
csv |
CSV with header row — open in a spreadsheet or feed into other scripts |
find-subscriptions --files export.csv --format json
find-subscriptions --files export.csv --format csv > subscriptions.csv
Sort orders
| Value | Meaning |
|---|---|
first_desc |
First charge date, newest first (default) |
first_asc |
First charge date, oldest first |
last_desc |
Most-recent charge, newest first |
last_asc |
Most-recent charge, oldest first |
count_desc |
Number of transactions, highest first |
count_asc |
Number of transactions, lowest first |
--inactive-for duration format
A number followed by year, month, or week (plurals accepted):
--inactive-for 6months
--inactive-for 1year
--inactive-for 3weeks
Subscriptions whose last transaction is older than the duration are hidden. Useful for trimming results to only currently-active charges.
Examples
Scan a single Amex export, auto-detecting the schema:
find-subscriptions --files Amex-2025.csv
Scan multiple files and force a schema:
find-subscriptions --files jan.csv,feb.csv --schema american_express
Filter out known/expected subscriptions and show only recent ones:
find-subscriptions --files Amex-2025.csv \
--known-payees data/known_payees.yml \
--inactive-for 6months \
--sort last_desc
Config file
Save your preferred defaults so you don't have to repeat flags every run.
1. Create a YAML config file:
# ~/.find-subscriptions.yml
sort: last_desc
format: text
min_amount: "5.00"
inactive_for: 6months
filter_known_payees: true
known_payees_path: ~/.config/find-subscriptions/known_payees.yml
2. Register it:
find-subscriptions --set-config ~/.find-subscriptions.yml
This writes the path to ~/.find-subscriptions-config-path. On every subsequent run, options from that file are loaded as defaults before any CLI flags are applied.
Option precedence (lowest to highest):
built-in defaults
CLI flags always win. The config file fills in anything you don't pass on the command line.
Supported config keys:
| Key | Type | Description |
|---|---|---|
sort |
string | Default sort order |
format |
string | Default output format |
min_amount |
string | Minimum recurring amount to show |
inactive_for |
string | Hide subscriptions older than this duration |
filter_known_payees |
boolean | Apply known_payees_path filter by default |
known_payees_path |
path | Default known-payees YAML path |
schema |
string | Default schema name |
from_date |
string (YYYY-MM-DD) |
Default --from date |
to_date |
string (YYYY-MM-DD) |
Default --to date |
schemas |
mapping | User-defined CSV schemas (see below) |
filesis intentionally not supported in the config — you must provide it on the command line each run.
User-defined schemas
Define custom CSV schemas in your config file without writing any Ruby code. This lets you analyze exports from banks not built into the tool.
Add a top-level schemas: key to your config YAML:
schemas:
my_bank:
required_headers:
- Date
- Description
- Amount
amount_key: Amount
direction: negative_debit
date_column: Date
date_format: "%Y-%m-%d"
payee_column: Description
Then use it by name:
find-subscriptions --files my_bank_export.csv --schema my_bank
Or let it auto-detect (your schema is checked after the built-ins, so unique headers help):
find-subscriptions --files my_bank_export.csv
Schema config keys
| Key | Required | Description |
|---|---|---|
required_headers |
yes | Array of CSV column names that must be present |
amount_key |
yes | Column name containing the transaction amount |
direction |
yes | How to determine if a transaction is outgoing (see below) |
date_column |
yes | Column name containing the transaction date |
date_format |
yes | strptime-compatible format string (e.g. %Y-%m-%d, %m/%d/%Y) |
payee_column |
yes | Column name containing the payee/description |
indicator_column |
if direction: indicator_column |
Column holding debit/credit labels |
debit_value |
no | Value in indicator_column that means "debit" (default: Debit) |
Direction strategies
| Strategy | When to use |
|---|---|
negative_debit |
Outgoing charges are negative numbers (most generic bank CSVs) |
positive_debit |
Outgoing charges are positive numbers (e.g. American Express) |
indicator_column |
A separate column labels rows as Debit or Credit (e.g. Navy Federal) |
indicator_column example:
schemas:
regional_bank:
required_headers:
- Posted Date
- Description
- Amount
- Transaction Type
amount_key: Amount
direction: indicator_column
indicator_column: Transaction Type
debit_value: Debit
date_column: Posted Date
date_format: "%m/%d/%Y"
payee_column: Description
Supported built-in schemas
| Name | Bank / Issuer | Required CSV headers |
|---|---|---|
american_express |
American Express | Date, Description, Amount, Card Member |
navy_federal |
Navy Federal Credit Union | Transaction Date, Description, Amount, Credit Debit Indicator |
generic |
Generic (YYYY-MM-DD dates) | Date, Description, Amount |
The schema is auto-detected from the CSV headers. Pass --schema NAME to override.
Known-payees file
The --known-payees flag points to a YAML file that maps canonical names to regex patterns. Any subscription whose payee matches a pattern is removed from output — useful for filtering charges you already know about.
- name: "Netflix"
normalized: "netflix"
patterns:
- '/\bnetflix\b/i'
- '/\bnflx\b/i'
- name: "Amazon Web Services"
normalized: "amazon web services"
patterns:
- '/aws\.amazon\.com/i'
Each entry requires:
name— human-readable label shown in output when the payee matchesnormalized— internal deduplication key (lowercase, used for grouping)patterns— list of Ruby regex literals in/pattern/flagsformat
data/known_payees.yml is the default file and is always loaded for payee normalization (display names). Filtering only applies when --known-payees is explicitly passed (or filter_known_payees: true is set in the config).
Output format
Subscriptions:
- SPOTIFY : $9.99 since January 2025 (14 transactions) until February 2026
- NETFLIX.COM : $15.49 since March 2024 (12 transactions) until February 2026
License
PolyForm Noncommercial License 1.0.0 — free to use and fork for personal and open-source projects. Commercial use prohibited. See LICENSE.