diff --git a/.bash_completion.d/planet b/.bash_completion.d/planet
new file mode 100644
index 0000000000000000000000000000000000000000..756b4f40a9f2e3444fac665410762fcc6e507089
--- /dev/null
+++ b/.bash_completion.d/planet
@@ -0,0 +1,25 @@
+# vim: ft=bash
+
+_planet () {
+    local cur prev words cword opts
+    _init_completion || return
+    opts='-h --help -l --list -C --config -b --browser'
+    browsers='mozilla firefox netscape galeon epiphany skipstone kfmclient
+    konqueror kfm mosaic opera grail links elinks lynx w3m windows-default
+    macosx safari google-chrome chrome chromium chromium-browser'
+
+    if [[ $prev == -C ]] || [[ $prev == --config ]]
+    then
+        _filedir '*.yaml|yml'
+    elif [[ $prev == -b ]] || [[ $prev == --browser ]]
+    then
+        COMPREPLY=($(compgen -W "$browsers" -- "$cur"))
+    elif [[ $cur == -* ]]
+    then
+        COMPREPLY=($(compgen -W "$opts" -- "$cur"))
+    else
+        COMPREPLY=($(compgen -W "$(planet -l)" -- "$cur"))
+    fi
+}
+
+complete -F _planet planet
diff --git a/.config/planet/config.yaml b/.config/planet/config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..88666755c06c646f301ad8ca0a8aaea604dc51ca
--- /dev/null
+++ b/.config/planet/config.yaml
@@ -0,0 +1,25 @@
+---
+general:
+  # For the list of available browsers, consult
+  # https://docs.python.org/3/library/webbrowser.html?highlight=webbrowser#webbrowser.register
+  browser: w3m
+sites:
+  shore: https://www.shore.co.il/blog/
+  #openbsd: https://openbsdnow.org/
+  openbsd: https://undeadly.org/
+  fedora: http://fedoraplanet.org/
+  debian: https://planet.debian.org/
+  gnome: http://planet.gnome.org/
+  kde: http://planetkde.org/
+  python: http://planetpython.org/
+  freedesktop: http://planet.freedesktop.org/
+  freebsd: http://planet.freedesktop.org/
+  kernel: http://planet.kernel.org/
+  foss-il: http://planet.hamakor.org.il/
+  sysadmin: http://planetsysadmin.com/
+  fsf: http://www.fsf.org/blogs/recent-blog-posts
+  hn: https://news.ycombinator.com/
+  phoronix: https://www.phoronix.com/scan.php?page=home
+  openbsd-current: http://www.openbsd.org/faq/current.html
+  debian-transitions: https://release.debian.org/transitions/index.html
+  lobsters: https://lobste.rs/
diff --git a/Documents/bin/planet b/Documents/bin/planet
new file mode 100755
index 0000000000000000000000000000000000000000..f83e5f909da83ecfbe32b157265c9dc8e2fd38af
--- /dev/null
+++ b/Documents/bin/planet
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+"""Planets and aggregators reader."""
+
+import argparse
+import pathlib
+import sys
+import webbrowser
+import xdg.BaseDirectory  # pylint: disable=import-error
+import yaml
+
+
+EXAMPLE_CONFIG = """---
+general:
+  # For the list of available browsers, consult
+  # https://docs.python.org/3/library/webbrowser.html?highlight=webbrowser#webbrowser.register
+  browser: w3m
+sites:
+  shore: https://www.shore.co.il/blog/
+"""
+
+
+def build_arg_parser():
+    """Build and return the argument parser."""
+    parser = argparse.ArgumentParser(description=__doc__)
+
+    parser.add_argument(
+        "-C",
+        "--config",
+        type=pathlib.Path,
+        help="Use a different configuration file.",
+    )
+    parser.add_argument(
+        "-l", "--list", action="store_true", help="List available sites."
+    )
+    parser.add_argument("-b", "--browser", help="Use a specific browser.")
+    parser.add_argument("site", nargs="?", help="Name of the site to open.")
+    return parser
+
+
+def get_config(path=None):
+    """Returns a configuration dictionary.
+
+    If a path is passed, that file path is used. Otherwise, use the default
+    path. If path is None and the default doesn't exist, an example file is
+    created."""
+    if path:
+        path = path.expanduser()
+        if not path.exists() and not path.is_file():
+            arg_parser.error("Configuration file does not exist.")
+
+    else:
+        base_dir = pathlib.Path(xdg.BaseDirectory.save_config_path("planet"))
+        path = base_dir / "config.yaml"
+    if not path.exists():
+        with open(path, "w", encoding="utf-8") as configfile:
+            configfile.write(EXAMPLE_CONFIG)
+            arg_parser.error(
+                f"Missing config file, generated an example one at {path}."
+            )
+    try:
+        with open(path, "r", encoding="utf-8") as configfile:
+            config = yaml.safe_load(configfile)
+    except Exception as exception:  # pylint: disable=broad-except
+        arg_parser.error(str(exception))
+    if "sites" not in config or not isinstance(config["sites"], dict):
+        arg_parser.error(
+            "Config file missing 'sites' key or 'sites' is not a dictionary."
+        )
+    return config
+
+
+def list_sites(config):
+    """Prints a list of sites from the config."""
+    for site in config["sites"].keys():
+        print(site)
+
+
+def open_site(config, site, browser_name=None):
+    """Opens a site from the config in a webbrowser."""
+    if browser_name == "":
+        browser_name = None
+    elif browser_name is not None:
+        pass
+    elif "general" in config and "browser" in config["general"]:
+        browser_name = config["general"]["browser"]
+    else:
+        browser_name = None
+    browser = webbrowser.get(using=browser_name)
+    if site not in config["sites"]:
+        arg_parser.error(f"Unknown site {site}.")
+    browser.open(config["sites"][site])
+
+
+if __name__ == "__main__":
+    arg_parser = build_arg_parser()
+    args = arg_parser.parse_args()
+    if not args.list and not args.site:
+        arg_parser.error(
+            "You must specify either site name or -l to list the available sites."  # noqa: E501
+        )
+    conf = get_config(args.config)
+    if args.list:
+        list_sites(conf)
+    else:
+        open_site(conf, args.site, args.browser)
+    sys.exit()