CLI tool
guyfedwards/nom avatar
guyfedwards/nom

nom reads RSS in the terminal and keeps the state in SQLite

RSS reader for the terminal

751 stars55 forksGoGPL-3.0

At a glance

What is it?
nom is a Go terminal RSS reader built on Bubbletea and Glamour, with local sync, offline reading and optional Miniflux or FreshRSS backends. It suits people who live in the shell and want feed state that survives a reinstall.
Who is it for?
Adopt nom if you read feeds in a terminal and want offline copies with read state kept in a file you can back up, and if you can live with a GPL-3.0 licence on a tool you modify. Do not pick it when you need a phone client, when your only feeds live behind Miniflux or FreshRSS, or when a plain reader and a browser extension would do.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A reader for people who never leave the shell

nom is described as a terminal based RSS feed reader, and the design bet is that the terminal can be a comfortable place to read articles. The rendering stack is the reason it can be. Article bodies are shown as styled markdown using Glow, the Charm project that renders markdown the way it would appear in a document, and the interface itself is a Bubbletea TUI, so navigation is done with key presses rather than a browser. The feature list is short and concrete: local sync and offline reading, backend connections with Miniflux and FreshRSS named as supported, vim style keybindings, and extras such as marking items read or unread, filtering and naming feeds. There is a demo animation in the repository's .github directory, which usually means the interface is the part the author wants to show. The audience is developers and terminal users who already keep a text editor open all day and want their reading list to live in the same place, including on a remote machine over SSH where a graphical reader is not an option.

Everything lives in one YAML config plus a SQLite file

The state model is easy to reason about and easy to back up. The configuration is a YAML file, and feeds live in a feeds section where each entry has a URL, a name and an optional list of tags:

yaml
feeds:
- url: https://dropbox.tech/feed
  name: DropBox
- url: https://snyk.io/blog/feed
  name: Snyk
  tags:
    - ai
    - tech

Read state and the downloaded articles are kept separately in a SQLite database, and nom uses sqlite as its store for feeds and metadata. That file sits next to the configuration in $XDG_CONFIG_HOME/nom/nom.db, and because it is an ordinary file it can be backed up like any file. It also holds the articles and the read state, which means the offline reading feature and the state you care about are the same artifact. Deleting it is a reset: nom starts from scratch and re-downloads every article with no state. The database filename can be overridden in the config, which is how you keep several independent setups, one file per configuration:

yaml
database: news.db

The module path in go.mod is github.com/guyfedwards/nom/v2 while the tagged releases are in the v3 series, so scripts that reference the module path and scripts that download a release binary are looking at different version numbers.

Feeds can be typed, added, edited in place, or imported

There are four ways to get feeds in, and they cover most onboarding situations. Typing them into the config is the direct one. The add command takes a URL and an optional name:

sh
nom add <feed_url> <optional feed_name>

The same command accepts flags for a name and for tags, which is how you attach the tag values used by filtering:

sh
nom add [-n <feed name>] [-t tag [...]] <url>

The third route is editing inside the TUI. Pressing E opens the configuration in your editor, and which editor that is depends on environment variables resolved in order of preference: $NOMEDITOR, then $VISUAL, then $EDITOR. After you edit the feeds you refresh with r, so the change takes effect in the running session. The fourth route is bulk import from an OPML file, which is the one that saves the most time when you are moving from another reader:

sh
nom import <path/to/opml|url/to/opm>

YouTube is treated as a special case because channel feeds are not linked from the channel page directly. The documented method is to open a channel and run a snippet in the browser console to derive the feed link:

js
console.log(
  `https://www.youtube.com/feeds/videos.xml?channel_id=${
    document.querySelector("link[rel='canonical']").href.split("/channel/")
      .reverse()[0]
  }`,
);

That prints a channel_id based feed URL that you can then add with nom add. Note that the URL comes from the console of a page you are visiting, so treat it as a snippet to understand rather than a permanent installation step.

Reading behaviour is set by a handful of top level switches

Most of the day to day behaviour is a single key in the config, which makes nom easy to tune without reading code. showread defaults to false and controls whether read items appear, and it can be toggled with M at runtime. autoread also defaults to false and marks items as read as soon as you select them or navigate over them. Ordering accepts asc, desc or length, where length sorts by the length of the article content in ascending order and unread items still sit at the top. A background refresh interval in minutes turns on automatic refreshing when it is set to anything other than zero:

yaml
showread: true
autoread: true
ordering: asc
refreshinterval: 5

Filtering has its own section, and the option that changes its behaviour the most is defaultIncludeFeedName. With it enabled, the feed name is included in the filtering query by default, so a plain keyword also matches items whose feed name contains that word. The documentation is blunt about the consequence: multi-feed filters stop working, for example a query combining f:xxx and f:yyy. Inside the reader you filter by title with the / character, and apart from plain keywords the qualifiers feed: (or f:) and tag: narrow results. The qualifier set is small enough to learn in a minute, which is what makes it usable at speed.

Miniflux and FreshRSS backends pull the feed list from a server

You can also point nom at an aggregator so that feeds arrive from a server rather than only from the local config. The backends section is a mapping, and each entry under miniflux or freshrss describes one server. Multiple backends can be declared and their feeds are all added:

yaml
backends:
  miniflux:
    - host: http://myminiflux.foo
      api_key: jafksdljfladjfk
    - host: http://yourminiflux.foo
      api_key: adfhdlsajfsifdi
  freshrss:
    - host: http://myfreshrss.bar
      user: admin
      password: muchstrong
      prefixCats: true # prefix feed name for freshrss entries

FreshRSS needs two specific things before it will work: API access has to be enabled in Settings > Authentication > Allow API access, and an API password must be set separately in Settings > Profile > API password, apart from your ordinary user password. The prefixCats option for FreshRSS prefixes feed names with their categories, which matters because nom's feed names are also the basis of filtering, so the naming scheme affects what you can search for later. One migration note for existing users: the V3 upgrade changed the config shape, and config.backends entries for Miniflux or FreshRSS have to be converted to arrays. The dependency list confirms the server side is a real integration rather than a thin wrapper, since the module requires miniflux.app/v2 directly.

Openers and proxies decide where a link actually goes

By default a link opens in your browser, but nom lets you route links to different commands based on a regular expression matched against the item link. The cmd string is run as a child process and %s marks where the link goes, while takeover decides whether the command takes over the terminal, which is what you want for something like lynx:

yaml
openers:
- regex: "youtube"
  cmd: "mpv %s"
- regex: ".*"
  cmd: "lynx %s"
  takeover: true

The order matters in practice, since a catch-all regex such as .* will swallow anything the earlier entries did not claim. Network access goes through the standard Go proxy mechanism, so if your traffic needs a proxy you set the usual environment variables and start nom:

sh
export HTTP_PROXY=https://proxy.example.com
export HTTPS_PROXY=https://proxy.example.com
nom

The quoted behaviour comes from the ProxyFromEnvironment documentation in the standard library: requests use the proxy whose variable matches their scheme, and NO_PROXY excludes hosts. Because these are ordinary environment variables, nom inherits whatever your shell already sets, which is convenient and also means an inherited proxy setting can surprise you.

Installation, packaging, and the GPL-3.0 licence

Binaries are published on the releases page, and the documented way to fetch one pipes a tarball straight into tar:

sh
curl -L https://github.com/guyfedwards/nom/releases/download/v3.0.0/nom_3.0.0_darwin_amd64.tar.gz | tar -xzvf -

To land the binary in /usr/local/bin in one step, the archive is piped into sudo tar with the target member named:

sh
curl -L https://github.com/guyfedwards/nom/releases/download/v3.0.0/nom_3.0.0_darwin_amd64.tar.gz |
sudo tar -C /usr/local/bin -xvzf - nom

A custom config file is then passed with the c flag, and the plain commands are short:

sh
nom -c my-custom-config.yml
sh
nom # start TUI
nom add <feed_url> <optional feed_name>
nom -h # see all available command and options

The Makefile builds with go build -o nom cmd/nom/main.go and runs tests with go test -v ./internal/..., and it needs CGO enabled because the SQLite driver is mattn/go-sqlite3, a cgo binding rather than a pure Go one. The Dockerfile reflects that: the builder stage installs gcc and alpine-sdk and sets CGO_ENABLED=1 before running make build, and the runtime image copies the resulting binary and starts it with the --config-path flag pointing at docker-config.yml. The project is GPL-3.0, which is the opposite of the permissive licence many CLI tools pick, so if you intend to modify and redistribute it, read the terms rather than assuming. The development pace is current: the last push was on 2026-10-07, and releases v3.3.1 and v3.3.2 came in June 2026 with v3.3.3 following on 2026-10-07.

Editorial conclusion

Adopt nom if you read feeds in a terminal and want offline copies with read state kept in a file you can back up, and if you can live with a GPL-3.0 licence on a tool you modify. Do not pick it when you need a phone client, when your only feeds live behind Miniflux or FreshRSS, or when a plain reader and a browser extension would do. Before committing, run nom import with an OPML file to see how your existing subscriptions translate into the feeds section of the config, confirm that the sqlite file in $XDG_CONFIG_HOME/nom/nom.db holds the read state you would hate to lose, and check that the version you install matches the module path your scripts expect.

Frequently asked questions

How do I install the nom RSS reader?

Binaries are published on the releases page and can be downloaded and unpacked with curl piped into tar, or extracted straight into /usr/local/bin with sudo tar. A custom configuration file is selected with the -c flag, for example `nom -c my-custom-config.yml`.

Where does nom store feeds and read state?

nom uses sqlite to store feeds and metadata, with the database file placed next to the configuration file in $XDG_CONFIG_HOME/nom/nom.db. It holds the downloaded articles and the read state, and deleting it resets nom so that all articles are downloaded again with no state.

Can I import my existing subscriptions into nom?

Yes, feeds can be imported from an OPML file with `nom import <path/to/opml|url/to/opm>`. Feeds can also be added with the add command, or edited inside the TUI by pressing E, which opens the configuration in the editor named by $NOMEDITOR, $VISUAL or $EDITOR.

Which backends does nom support?

Miniflux and FreshRSS are the supported backends, and each is configured in the backends section of the config file with a host and its credentials. FreshRSS additionally needs API access enabled and a separate API password set in its settings.

How do I filter articles in nom?

Press / to filter by title. Plain keyword searches are supported, and the feed: (or f:) and tag: qualifiers narrow results to specific feeds or tags. With defaultIncludeFeedName enabled the feed name is part of the query by default, which means multi-feed filters do not work.

Official sources

  1. guyfedwards/nom on GitHub
  2. Issues
  3. License: GPL-3.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/guyfedwards-nom.svg)](https://hysenlabs.com/projects/guyfedwards-nom)

Was this useful?