myrssfeed
Read RSS/Atom feeds, and print only the items that match criteria you describe in plain English. Claude makes the call on each item; you get the headline, the link, and a one-line reason.
$ myrssfeed --prompt "Export controls on AI chips, and EU tech regulation" \
--feed https://feeds.arstechnica.com/arstechnica/technology-lab \
--feed https://www.theregister.com/headlines.atom
US tightens accelerator export rules for third-country resellers
https://www.theregister.com/2026/10/06/export_rules/
The Register · 2026-10-06T09:12:00+00:00
why: Directly about chip export controls.
EU Commission opens consultation on cloud switching rules
https://arstechnica.com/2026/10/06/eu-cloud/
Ars Technica · 2026-10-06T07:40:00+00:00
why: EU tech regulation affecting cloud providers.
Everything else in those feeds is dropped silently.
Why
Feed readers filter on keywords, which is both too narrow ("accelerator" misses "GPU") and too broad (every mention of "chips" matches). A prompt describes what you actually want: "new CVEs in Python packages I'd have to patch, not general security commentary."
Install
myrssfeed is not on PyPI yet, so install it from a checkout:
git clone https://github.com/fabianhogger/myrssfeed
cd myrssfeed
pip install .
For development, install it editable with the test and lint tools:
pip install -e ".[dev]"
Once the project is published (see Releasing), the usual one-liner will work:
pip install myrssfeed # not available until the first release is published
Requires Python 3.9+ and an Anthropic API key:
export ANTHROPIC_API_KEY=sk-ant-...
Check that it landed:
myrssfeed --version
If the myrssfeed command is not found, your Python scripts directory is not on
PATH; python -m myrssfeed works identically.
Usage
One-off run
myrssfeed --prompt "Rust async ecosystem news" --feed https://blog.rust-lang.org/feed.xml
With a config file
myrssfeed.toml:
system_prompt = """
Keep items about:
- security advisories affecting Python or Node packages
- outages at major cloud providers
Skip funding rounds, opinion pieces, and conference announcements.
"""
feeds = [
"https://feeds.feedburner.com/TheHackersNews",
"https://status.cloud.google.com/en/feed.atom",
]
interval = "30m"
state_path = "~/.local/state/myrssfeed/state.json"
output_format = "markdown"
myrssfeed --config myrssfeed.toml
JSON config files work the same way; the format is chosen by file extension.
Scheduling
Two options, depending on whether you want a resident process.
Built-in loop — runs until you stop it, and shuts down cleanly on
Ctrl-C/SIGTERM so the state file is never left half-written:
myrssfeed --config myrssfeed.toml --interval 45m
cron or systemd — have myrssfeed run one pass and exit. The state file means each run only reports items it has not reported before:
*/30 * * * * ANTHROPIC_API_KEY=sk-ant-... /usr/local/bin/myrssfeed --config ~/myrssfeed.toml --once >> ~/myrssfeed.log 2>&1
A systemd timer unit is in examples/.
Checking a setup before spending anything
--dry-run fetches the feeds and lists what would be judged, without calling
the API, and without marking anything as seen:
myrssfeed --config myrssfeed.toml --dry-run -v
Output formats
--format |
For |
|---|---|
text |
Reading in a terminal (default) |
markdown |
Pasting into notes, or posting to chat |
json |
Piping into jq or another program |
Logs go to stderr and results to stdout, so myrssfeed ... | jq works as expected.
How it works
- Each feed is fetched with
If-None-Match/If-Modified-Since, so an unchanged feed costs one small request and no tokens. - Entries already handled on an earlier run are dropped using the state file.
- The rest are sent to Claude in batches, with your prompt as the system prompt and a JSON schema constraining the reply to one verdict per item.
- Matching items are printed; every item that got a verdict is recorded as seen.
Items in a batch that fails (a transport error, a declined request) are not recorded, so the next run retries them rather than dropping them silently.
Cost
Only titles, summaries and dates are sent — never full articles — and summaries are truncated. The system prompt is cached across the batches of a run.
The knobs that matter:
--batch-size(default 10): larger batches spread the prompt over more items.--effort(defaultlow): relevance is a classification task and rarely needs more. Raise it if the criteria are subtle.--no-reasons: drops the per-item justification, and the output tokens with it.--max-items N: caps how many entries per feed are considered at all.
Configuration reference
Every key below can be set in a config file, as an environment variable
MYRSSFEED_<KEY> (uppercase), or as a command-line flag. Precedence is
file < environment < flags.
| Key | Default | Meaning |
|---|---|---|
system_prompt |
required | Your criteria |
feeds |
required | Feed URLs (comma-separated in the environment) |
api_key |
ANTHROPIC_API_KEY |
Anthropic API key |
model |
claude-opus-5-5 |
Claude model id |
effort |
low |
low, medium, high, xhigh, max |
batch_size |
10 |
Items per API request |
max_items_per_feed |
50 |
Newest entries considered per feed; 0 for no limit |
state_path |
none | Where handled items are recorded |
state_ttl_days |
30 |
How long they are remembered |
output_format |
text |
text, markdown, json |
interval |
none | Run repeatedly, e.g. 30m, 2h, 1h30m |
timeout |
120 |
Per-request HTTP timeout, seconds |
max_retries |
3 |
SDK retries on transient failures |
max_tokens |
16000 |
Output ceiling per request |
refusal_fallback |
true |
Route a declined request to a fallback model |
include_reasons |
true |
Ask for and show a reason per item |
user_agent |
myrssfeed (...) |
User-Agent used when fetching feeds |
--api-key also exists, but prefer the environment variable: flags are visible
in ps output and end up in shell history.
Exit status
| Code | Meaning |
|---|---|
0 |
Ran successfully (whether or not it matched) |
1 |
The run failed |
2 |
Bad arguments or configuration |
3 |
Ran, but some feeds or batches failed |
Library use
The CLI is a thin wrapper; the same thing works from Python:
from myrssfeed import Config, Runner
config = Config(
system_prompt="Release notes for Python web frameworks.",
feeds=["https://blog.djangoproject.com/rss/"],
state_path="~/.local/state/myrssfeed/state.json",
)
result = Runner(config).run_once()
for match in result.matches:
print(match.item.title, match.item.link, "-", match.verdict.reason)
Runner accepts a reader, a relevance_filter and a state if you want to
substitute your own — which is how the test suite avoids the network entirely.
Writing a good prompt
The prompt is the whole filter, so be concrete about both halves:
Keep:
- Postgres release announcements and migration-breaking changes
- Write-ups of real production incidents with root-cause detail
Skip:
- Vendor benchmarks and marketing posts
- "Top 10 database tips" listicles
- Anything about MySQL unless it compares directly to Postgres
The model is told to prefer precision over recall, so tangential items are
dropped. If you are missing things you wanted, say so in the prompt explicitly
("err on the side of including borderline items") or raise --effort.
Development
pip install -e ".[dev]"
pytest # no network or API calls
ruff check . && ruff format --check .
mypy
The test suite never reaches the network or the Anthropic API: feeds are parsed from in-memory XML and the classifier is a scripted fake, so it runs offline and without an API key.
Releasing
Publishing uses trusted publishing, so no API token is stored in this repository. One-time setup, done once per index at https://pypi.org/manage/account/publishing/ and the same page on https://test.pypi.org:
| Field | Value |
|---|---|
| Project | myrssfeed |
| Owner | fabianhogger |
| Repository | myrssfeed |
| Workflow | release.yml |
| Environment | pypi on PyPI, testpypi on TestPyPI |
Then, per release:
# 1. Rehearse on TestPyPI, and check the result installs
gh workflow run release.yml
pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ myrssfeed
# 2. Bump __version__ in src/myrssfeed/__init__.py, move the CHANGELOG entry out
# of [Unreleased] into a version heading, then tag and push
git commit -am "Release 0.1.0"
git tag -a v0.1.0 -m "0.1.0"
git push origin main v0.1.0
The tag push is what publishes to the real PyPI. The workflow refuses to upload
if the tag does not match __version__, because PyPI never allows a version
number to be reused — a mistake means yanking the release and shipping a new
number, so it is checked before the upload rather than after.
To inspect a build locally first:
pip install build twine
python -m build
twine check --strict dist/*
License
MIT.
Metadata
Release files for myrssfeed 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| myrssfeed-0.1.0.tar.gz | 41.6 kB | With provenance | Details for myrssfeed-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| myrssfeed-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for myrssfeed-0.1.0-py3-none-any.whl |
Total release size: 75.2 kB
Release files / myrssfeed-0.1.0.tar.gz
| Download URL | myrssfeed-0.1.0.tar.gz |
|---|---|
| Size | 41.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4273511687bdaa9620ba114bf770e67c0efc58ef3d3964bfcf43484c87f6bcb3
|
|
BLAKE2b-256 checksum How to use checksums |
d2f6990c5b01e2e8c8ede536f119e87cd502a791a759c6b3bed4f8e6b98c842a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / myrssfeed-0.1.0-py3-none-any.whl
| Download URL | myrssfeed-0.1.0-py3-none-any.whl |
|---|---|
| Size | 33.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d7cfa898a962e6dddfcfee14193eb1b11eb41a789d65b87690fa25f6659d18ed
|
|
BLAKE2b-256 checksum How to use checksums |
102b73430e050943d0862522b18e0a1541ff2f13a4f6190337e7b6e27be730d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logSecurity for myrssfeed 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for myrssfeed-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.