An unofficial terminal UI for /dev/null, the developers' commit social network — in any terminal, or as a Herdr pane.
Unofficial: not affiliated with /dev/null. It reads the same pages the website serves its guests.
Every post on /dev/null is a git commit drawn on a rail, coloured by its
author's lane: a small circle before the name, coloured from the author's real
picture, the short hash on the right, and bodies with inline code, links,
quotes, polls, link previews and replies. A body longer than five lines is
clamped in the log, replies included; enter opens the commit with its whole
conversation. ← goes back, and c / R commit and reply as you, signed with
your SSH key: the composer takes @ mentions and dropped images, the way the
website does.
⬤ legecha @legecha · 4m 40dd829
│ I haven't done social media for years.
│ But this feels different.
│ Hello, World!
│ ★ 3 ↳ 1
○
Each author gets a dot in the lane colour, tinted by a colour sampled from
their real GitHub photo, so the rail still reads as a person and costs two
columns. a turns the dots off and back on.
A commit's attached images stay a one-line ▣ 2 images marker until you ask
for them: i draws the pictures under the commit and i again puts them away,
so the log is text unless you want to look. Where the terminal speaks the Kitty
graphics protocol — Ghostty, Kitty, WezTerm, and Herdr forwards it from a pane —
the real picture is drawn; everywhere else it falls back to half blocks, which
need no protocol and survive any redraw.
The log, latest or trending, 20 commits a page:
A commit opened with its replies, and the hint bar for the keys that work there:
- Node.js 20 or newer (
fetch,Intl.Segmenter). - Herdr 0.9.0 or newer only for the plugin pane; the CLI needs no Herdr.
- One WebP converter for attached images —
dwebp,sips(macOS),ffmpeg, ImageMagick orvips. /dev/null serves its images as WebP and Node cannot read that; without a converter the marker says so and the rest of the pane is unaffected. Reading, committing and replying need none of this. - OpenSSH 8.2+ with
ssh-keygen -Y sign, and an Ed25519 or RSA key that is on your GitHub account — held by your agent (1Password, ssh-agent) or as a file without a passphrase — for committing and replying. FIDOsk-*keys and ECDSA keys are not accepted by /dev/null. Reading needs none of this. - Following and the inbox are the user's own, so they need an approved device
(the same one-time approval stars and deletes use: press
s). The pane catches the approval code itself on macOS (osacompile) and Linux (xdg-mime), and falls back to pasting it when neither is available.
As a CLI, with a package manager:
npm install -g devnull-tui
bun add -g devnull-tui
pnpm add -g devnull-tui
npx devnull-tui --help # without installing anything
bunx devnull-tui --helpWithout one, no build step:
curl -fsSL https://raw.githubusercontent.com/arhen/devnull-tui/main/install.sh | shThe script needs curl and Node 20+, puts the files in ~/.local/share/devnull
and points ~/.local/bin/devnull at them (DEVNULL_HOME, DEVNULL_PREFIX and
DEVNULL_REF override that). Run from a checkout, sh install.sh installs that
checkout instead. To remove it, delete both paths.
As a Herdr plugin:
herdr plugin install arhen/devnull-tui
herdr server reload-configThe old arhen/herdr-devnull-plugin entry still installs: it runs the current
code from here, so listings and keybindings made before the rename keep working.
From a checkout, while developing:
herdr plugin link ~/Code/devnull-tuiFrom a pane, beside that pane:
herdr plugin pane open --plugin devnull.feed --entrypoint feed \
--direction right --target-pane "$HERDR_PANE_ID" --focusFrom the command palette or a keybinding, the action opens or focuses a feed in the workspace the user is looking at:
herdr plugin action invoke devnull.feed.openBind a key in ~/.config/herdr/config.toml:
[[keys.command]]
key = "prefix+d"
type = "plugin_action"
command = "devnull.feed.open"
description = "open the /dev/null feed"The pane opens as a split beside the calling pane. Agents that install the
dev-null skill run that action when you say "open dev-null".
devnull is the same program without Herdr, in any terminal. From a checkout,
node index.js.
| Flag | |
|---|---|
--once |
print the log and exit |
--log <name> |
latest (default), trending, following, notifications |
--interval <sec> |
refresh interval, at least 15 (default 60) |
--width <cols> |
width for --once (default: the terminal's) |
--help / -h |
usage |
--version / -v |
the version |
Everything works without Herdr: reading, following, the inbox, stars, deletes,
committing and replying with your SSH key, device approval, dropping images
into a draft. The only difference is o: a Herdr browser tab when Herdr is
there, the default browser when it is not. A pipe is treated as a reader —
devnull | less and devnull > feed.txt print once and exit.
| Key | |
|---|---|
j / k |
next / previous commit |
→ / ← |
open the selected commit / go back one level |
g / G |
first / last commit |
ctrl+d / ctrl+u |
half a page |
enter |
open the commit and its conversation (same as →) |
e |
expand the selected reply's own replies, or a group in the inbox |
R |
reply to the selected commit |
c |
commit something new |
o |
open the commit in a browser (Herdr browser tab, else the default browser) |
a |
avatars: colour dots on / off |
i |
images of the selected commit: show / hide |
s |
star or unstar the selected commit · approve this pane as a device |
d |
delete one of your own commits, twice to confirm |
r |
refresh the open tab |
1 / 2 / 3 / 4 |
latest · trending · following · notifications |
n |
jump to notifications from anywhere |
t |
latest / trending |
? |
keys |
esc |
back one level: reply → its parent → timeline |
q |
close the pane; in a commit, back to the timeline |
The tab row numbers the tabs with the keys that open them, so 1 latest,
2 trending, 3 following, 4 notifications — the pane opens on 1, so a
fresh install reads the public log before it asks for anything. Following and
the inbox are read from the signed API: without a device the tab says so and
s starts the approval. The inbox refreshes only while it is on screen, because listing
notifications marks them read on /dev/null, as it does on the web.
c and R open a composer that works like the one on the website:
@tags people. The names on screen come first, then /dev/null's own people search;↑/↓(orctrl+p/ctrl+n) pick one,taborenterputs the@handlein,esccloses the list.- Drop an image into the composer, or paste its path. Herdr pastes a dropped
file as its path, so a drop, a Finder drag, or a typed
--image-style path all attach the same way: JPEG, PNG or WebP up to 10 MB, at most 3 a commit; videos, GIFs, SVG and HEIC are refused with the reason, as /dev/null would refuse them. On an empty draft, backspace removes the last image. entersends,shift+enterstarts a new line,esccancels,ctrl+uclears the draft.- The composer is two lines tall.
←/→move the caret,ctrl+←/ctrl+→(oralt+←/alt+→, which Herdr sends asalt+b/alt+f) move by word,ctrl+a/ctrl+e(cmd+←/→) jump to the start or end of the line,ctrl+kdeletes to its end,deletedeletes forward.↑/↓walk the caret through a longer draft, and the hint says how many lines are out of view.
Stars, pastéis, follows, bookmarks and deletes are not part of the commit
signature API: /dev/null only accepts them from a device key it has approved.
Press s once and the UI walks you through that approval, the same PKCE
flow the mobile app uses:
- It makes an Ed25519 key in its state directory (
HERDR_PLUGIN_STATE_DIRinside Herdr,~/.cache/devnull-tuiotherwise) and opens/mobile/authorizein your browser. - You approve it there. The page answers on
devnull://device?code=…, which a desktop has no handler for, so for the few minutes the approval takes the pane registers one: an AppleScript applet in~/Applicationson macOS (made withosacompile, so no compiler or binary is needed), anx-scheme-handler/devnullentry in~/.local/share/applicationson Linux. It writes the code where the app can read it, and both are removed as soon as the approval is done. - When no handler can be installed, it asks for the code instead:
escswitches to pasting it, and a code stays good for five minutes.
From then on s stars and unstars, d deletes your own commits (twice to
confirm), and the Following tab and the inbox open. Every call is signed as
timestamp\nnonce\nMETHOD\npath\nbody with ssh-keygen -Y sign -n devnull-api
and sent with X-Devnull-Timestamp, X-Devnull-Nonce and X-SSH-Signature.
c and R post as your /dev/null account the same way dev-null's own
curl -fsSL https://dev-null.sh/commit | bash does, with no login:
- It lists your public keys (
ssh-add -Land~/.ssh/*.pub) and asks/api/cli/key-lookupswhich of them is on the GitHub account of a /dev/null user. Public keys are public; nothing else leaves your machine. - It signs
timestamp\nnonce\nreply-to\nbodywithssh-keygen -Y sign -n devnull-commit, so your private key — held by 1Password or ssh-agent — never leaves the agent. With images the payload gains a line of their SHA-256s and the signature moves todevnull-commit-images; the images themselves go up as a multipart form next to the signed payload, exactly like the script's--image. - It posts the payload, the images and the signature to
/api/cli/commits.
Stars, pastéis, follows and bookmarks are not available here: /dev/null
only accepts them from an approved device (a key you approve under
/settings/devices from the mobile app), and this UI does not pretend
otherwise.
It reads what the website renders for guests, so there is no API key, no cookie jar, and no scraping of private state:
https://dev-null.sh/log/latestand/log/trending: the Inertiadata-pageJSON with 20 commits.https://dev-null.sh/@user/commits/{id}withCookie: replies_per_page=25: a commit and its replies, one page at a time.
Following and the inbox are the user's own, so they come from the signed API the mobile app uses, with the device key the app already has:
/api/v1/log/following: the commits of the people you follow, plus/dev/null's suggestions while the log is empty./api/v1/notifications?type=all: the inbox grouped and filtered the way the web groups it — follows, stars, replies, quotes, mentions, moderation and level-ups, each group with its actors, its event and unread counts, and the commit it happened to./api/v1/notifications/{id}/group?type=all: the eventseopens under a group.
Listing notifications marks them read, as on the web, so the pane only asks
while the inbox tab is open. The open tab refreshes every 60 seconds;
--interval changes that.
For agents and scripts, and for a quick look without a TUI. Piped output has no colour codes and no key hints:
devnull --once --log latest --width 100
devnull --once --log notifications # the inbox, as text
devnull --once # one text frame, when pipedFrom a checkout, node index.js takes the same flags. The Herdr actions call
the same mode: herdr plugin action invoke devnull.feed.latest prints it into
the plugin log (herdr plugin log list --plugin devnull.feed); following and
notifications are actions too.
npm install # typescript and @types/node, for checking only
npm run typecheck # tsc --noEmit over the JSDoc-checked sources
node index.js # run it by handThere is no build step and no runtime dependency. devnull is node index.js
with a shebang; inside Herdr, the manifest launches /bin/sh run.sh, which
execs the same file.
| Path | |
|---|---|
herdr-plugin.toml |
pane, actions, keybinding target |
install.sh |
the curl installer: files under ~/.local/share/devnull, a link in ~/.local/bin |
run.sh, open.sh, find-node.sh |
the Herdr entry points |
index.js |
state machine: timeline, thread, composer, keys, polling |
lib/draw.js |
rows, rails, avatars, links, thread, inbox, composer and chrome |
lib/devnull.js |
page JSON → commits, threads |
lib/api.js |
device-signed Following and notifications → the same shape |
lib/catcher.js |
the temporary devnull:// handler that catches the approval code |
lib/avatars.js |
GitHub avatar photo → one colour sample for the dot, cached in HERDR_PLUGIN_STATE_DIR |
lib/attachments.js |
dropped paths → readable images, by sniffed content, under /dev/null's limits |
lib/pictures.js |
attached images → WebP converted to PNG, then half-block rows, cached in HERDR_PLUGIN_STATE_DIR |
lib/auth.js |
SSH key lookup, ssh-keygen -Y sign, commit/reply with images |
lib/jpeg.js |
the baseline JPEG decoder GitHub's photo avatars need |
lib/pane-id.js |
finds an already-open feed pane for open.sh |
lib/text.js |
display width, wrapping, link detection |
MIT.

