A message-of-the-day generator that's half facts, half feelings.
Like fastfetch, but it tells you that your VPN is down, your scrub is overdue, and you have 12 packages to upgrade — instead of telling you which kernel you're running for the seven-thousandth time. With a friendly animal.
_y~~~~__ ____ host cube
~. ````` `~_ up 4d 12h 31m
~ :~ load 0.42 0.38 0.31
F L disk 97G / 888G (10%)
: F 204G / 32T (0%)
_yyy` _ F mem 8G / 16G (50%)
`: _y L .y____y_~y` L ip 10.10.10.10 (eth)
~_``` : : F $ 4 100.121.19.125 (tailscale)
`~` y: : y~ .F 4
`~~~ `yyy__y
● 2 services running → motd services
⚠ 12 packages outdated → motd updates
(That's an elephant. There are 11 other animals to choose from.)
- Mark on the left: a colorful animal of your choosing — pick a different one per host so you know where you logged in
- Facts on the right: hostname, uptime, load, disk, memory, IP — all driven by tiny built-in collectors so it's <50ms warm
- Alerts below: each module reports a one-liner only when something's worth saying; the line vanishes when it's not
- Drill-down:
motd <name>runs that module's full version (e.g.motd diskshows a per-mount breakdown with bars + ZFS datasets) --watchmode: edit the config in one pane, watch motd re-render on save in another- Cache where it makes sense: per-module TTL for things that genuinely don't change often, no cache for things that respond to user action
curl -L https://github.com/arne/motd/releases/latest/download/motd_Linux_x86_64.tar.gz | tar xz
sudo install -m 0755 motd /usr/local/bin/motdgo install github.com/arne/motd/cmd/motd@latestThat's enough — with no config file, motd ships with a built-in default that picks a random animal and shows the standard system facts. To customize, drop a config in place:
# user config (just for you)
mkdir -p ~/.config/motd
motd example-config > ~/.config/motd/config.yaml
# or system-wide for every user on the box
sudo mkdir -p /etc/xdg/motd
motd example-config | sudo tee /etc/xdg/motd/config.yaml >/dev/null
git clone https://github.com/arne/motd /tmp/motd-src
sudo cp -r /tmp/motd-src/examples/marks /etc/xdg/motd/# fish
echo "motd" >> ~/.config/fish/config.fish
# bash
echo "motd" >> ~/.bashrc
# zsh
echo "motd" >> ~/.zshrcSingle YAML file. Two halves: layout (a recursive tree of stacks) and modules (named things that produce text).
mark: { animal: panda }
layout:
vstack:
- hstack:
- mark
- vstack: [host, up, load, disk, mem, ip]
- vstack: [services, updates, scrub]
modules:
host: { builtin: hostname }
up: { builtin: uptime }
load: { builtin: load }
mem: { builtin: mem }
ip: { builtin: ip }
disk: { builtin: disk }
services:
builtin: services
list:
- { name: immich, http: "http://localhost:2283/api/server/ping" }
- { name: openwebui, cmd: "incus exec openwebui -- curl -sf http://localhost:8080/health" }
updates:
command: |
n=$(apt list --upgradable 2>/dev/null | tail -n +2 | wc -l)
[ "$n" -gt 0 ] && echo "$n packages outdated"
command_full: "apt list --upgradable"
scrub:
command: |
last=$(zpool status storage 2>/dev/null | grep -oP 'on \K.*$' | head -1)
if [ -n "$last" ]; then
days=$(( ( $(date +%s) - $(date -d "$last" +%s) ) / 86400 ))
if [ "$days" -gt 30 ]; then
echo "storage scrub last ran ${days}d ago"
fi
fi
command_full: "zpool status -v"
cache: 6h| Name | What it shows |
|---|---|
hostname |
The hostname, in your terminal's primary accent |
uptime |
4d 12h 31m, friendly format |
load |
0.42 0.38 0.31 |
mem |
8G / 16G (50%) from /proc/meminfo |
ip |
Non-loopback IPv4 addresses, with friendly category labels (eth, wifi, tailscale, incus, docker, wg, vpn, zerotier, ...) |
disk |
Root + auto-detected zpool roots; motd disk shows per-mount table with utilization bars and ZFS datasets |
services |
Heterogeneous health checks: http, tcp, unit (systemd), or cmd. motd services shows a per-service status table |
Anything you can put in a shell command. Output goes through the styling pipeline.
weather:
command: "curl -s wttr.in/?format=3"
command_full: "curl -s wttr.in/"
cache: 30m
uptime_kuma:
command: "~/bin/check-status-page"Convention — for command-based modules:
- Empty stdout = silent (no alert renders)
- Any stdout = treated as a warning, framework prepends ⚠ glyph and appends drill-down hint
- Exit nonzero = treat as failure; keep last good cache, don't overwrite
command_fullis whatmotd <name>runs — use it to dump the verbose view
Reserved names. version, update, refresh, and example-config are subcommands; modules with those names won't be reachable via motd <name>.
12 cute animals ship embedded in the binary. Pick one per host to differentiate where you logged in:
bear cat dog dragon fox lion
monkey mouse panda penguin piggy unicorn
Reference by name — no marks directory needed:
mark: { animal: mouse }If you'd rather use your own file, mark: { file: path/to/foo.ansi } still works. Either way, if the name (or file) doesn't resolve, motd substitutes a random shipped animal rather than erroring.
Want one we don't ship? Find its emoji codepoint at openmoji.org and run:
scripts/make-animal-mark.sh <name> <CODEPOINT>
# e.g. scripts/make-animal-mark.sh chicken 1F414The pipeline: download OpenMoji SVG → render to high-res PNG → trim → chafa-render to colored ASCII at 24×14 cells.
Each module is also accessible directly:
motd services # full status table for all services
motd disk # per-mount breakdown with bars + ZFS datasets
motd updates # apt list --upgradableIf a module has a command_full set, that's what runs. Builtins like disk and services ship their own drill-down.
motd --watchRe-renders whenever the config or any referenced file changes. Edit YAML in one pane, see the result in another. Ctrl+C to exit. Watches the config file's directory and the mark file's directory; debounces multi-event editor saves.
| fastfetch | cowsay | motd | |
|---|---|---|---|
| System facts | ✓ | ✓ | |
| Actionable alerts | ✓ | ||
| Configurable layout | partial | ✓ | |
| Drill-down | ✓ | ||
| Friendly animal | ✓ | ✓ | |
| Re-renders on config save | ✓ | ||
| Useful out-of-the-box | ✓ | ✓ | ✓ |
| Cute, in 50ms | ✓ |
In order:
--config <path>flag$MOTD_CONFIGenv var~/.config/motd/config.yaml(if it exists)/etc/xdg/motd/config.yaml(system fallback)- Built-in default — used when none of the above resolve. A random animal mark plus host/uptime/load/mem/ip/disk. No config file is created on disk; just run
motd example-config > ~/.config/motd/config.yamlwhen you're ready to customize.
If --config or $MOTD_CONFIG points at a file that doesn't exist, that's an error — the built-in default only kicks in when no path was specified.
Mark file paths in config are resolved relative to the config file's directory, so marks/animals/panda.ansi works whether you're using the system or user config.
Replaces the running binary with the latest release from GitHub. Verifies the SHA256 against the release's checksums.txt before swapping the file in place.
motd update # download and install if newer
motd update --check # report current vs. latest, install nothing
motd update --force # reinstall even when already on the latestIf the binary lives somewhere only root can write (e.g. /usr/local/bin), re-run with sudo. For Go-installed builds (go install), ~/go/bin is user-writable so no sudo needed.
Prints the version, commit, and build date. Equivalent to motd --version.
Wipes the per-module cache. Run it after an action that invalidates a cached module — most commonly apt upgrade, where the updates count would otherwise stay stale until its TTL expires.
sudo apt upgrade && motd refreshFor a single-user box, you can wire it into apt with a dpkg hook. The cache is per-user (~/.cache/motd), so this only refreshes the cache for the user who invoked sudo apt; other users keep their own caches and will see stale counts until their TTL expires or they run motd refresh themselves. The hook also no-ops under unattended-upgrades and other non-sudo apt invocations, since $SUDO_USER is unset there. Use the absolute path to your motd install — root's PATH typically doesn't include ~/go/bin.
sudo tee /etc/apt/apt.conf.d/99-motd-refresh <<'EOF'
DPkg::Post-Invoke { "[ -n \"$SUDO_USER\" ] && sudo -u \"$SUDO_USER\" /usr/local/bin/motd refresh >/dev/null 2>&1 || true"; };
EOFPrints a sample config to stdout. Pipe it into a file and edit from there:
mkdir -p ~/.config/motd
motd example-config > ~/.config/motd/config.yamlgit clone https://github.com/arne/motd
cd motd
go build -o motd ./cmd/motd
./motd --config examples/config.yamlReleases are built via goreleaser on tag push. See .goreleaser.yml for the build matrix.
MIT. See LICENSE.