Files
omarchy-setup/bin/omarchy-handoff
T
noconnorandClaude Opus 5 50176b78ee Sync omarchy-air's handoff work: pointer patch, firewall, case fix
Everything from the air side of lan-mouse that was living only on that
machine, plus corrections to what this repo already said.

New: handoff/patch/ is a two-line source patch making the daemon forward
the ACCELERATED pointer deltas. Upstream 0.11.0 forwards dx_unaccel and
discards the accelerated pair in the same event, and the wlr receiver
applies no curve either, so remote motion arrives with the sender's
acceleration stripped and nothing replacing it -- and sensitivity /
accel_profile are inert on both ends, so no config can fix it. The patch
also adds LAN_MOUSE_POINTER_SCALE for the residual logical-ppi gap
(0.7 for air's 140.1ppi against the LG's ~95.8). install.sh builds it
per-machine; the unit now runs the result. This laptop is still unpatched,
so its forwarded motion is still wrong until the handoff step runs here.

New: install.sh --handoff opens 4242/udp for the peer in ufw. ufw is
default-deny inbound and a dropped inbound packet presents IDENTICALLY to
a bad certificate; this cost a day. Nothing in the repo covered it.

Fixed: cmd_restart bypassed the systemd unit with pkill + uwsm-app, which
races Restart=on-failure and can leave two daemons on 4242. It now prefers
the unit, matching what cmd_log already assumed, and keeps the hand-start
path only for when no unit exists.

Documented, and this was the actual root cause of a day of dead crossings:
fingerprints are CASE-SENSITIVE. lan-mouse generates them lowercase and
compares with a plain map lookup, while openssl prints uppercase and
cli authorize-key stores what it is handed. An uppercase digest is the
right digest, valid TOML, non-empty, passes every guard, and never
matches -- the handshake dies bad_certificate. Also documented:
authorization is receiver-side only (connect.rs sets insecure_skip_verify),
so each direction depends solely on the receiver's list and a sender cannot
tell whether it is talking to the real peer.

Corrected: README claimed the unknown-device* accel rule was needed because
the receiver double-accelerates already-accelerated deltas. That is backwards
and the rule is a measured no-op -- the device matches, but sensitivity and
accel_profile are libinput settings and a wlr-virtual-pointer is not a
libinput device. Measured on both machines: 398x198px travel at
sensitivity 0 and identical at -0.9, exactly 1:1. Lines kept (harmless, and
the naming note is worth having) with the real mechanism written down.

~/.config/lan-mouse/ stays out of the repo as before; handoff/config/ holds
example configs carrying the backend, edge and lowercase rules instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LrgrMhPG8CTo9XZF326rjZ
2026-10-10 17:06:34 -04:00

514 lines
21 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Share keyboard and mouse with a nearby Omarchy machine
# omarchy:group=handoff
# omarchy:args=[status|toggle|on|off|position <edge>|peer <host> <ip>|fingerprint|pair <desc> <sha256>|restart|log [-f|N]|fixcase|doctor]
# omarchy:examples=omarchy-handoff position right
# omarchy:examples=omarchy-handoff toggle
# NOTE: the omarchy: metadata above is cosmetic. Omarchy's dispatcher only scans
# its own bin dir, so `omarchy handoff ...` does NOT work -- invoke this as the
# bare `omarchy-handoff`.
# HARD-WON FACTS -- kept here because lan-mouse rewrites config.toml on every
# CLI activate/deactivate (i.e. every SUPER+SHIFT+K) and strips its comments.
# Startup activation does NOT rewrite it; only the CLI path does.
# * capture_backend MUST be "layer-shell". On Hyprland 0.56.2 the
# "input-capture-portal" backend is a SILENT NO-OP: it initializes cleanly,
# org.freedesktop.portal.InputCapture answers on D-Bus (v1, caps=3), and it
# creates ZERO barriers. No edge ever fires and nothing is logged.
# Verify with: lan-mouse --capture-backend <b> test-capture
# and look for "adding capture for position X - using outputs: [NON-EMPTY]".
# Note layer-shell logs an EMPTY pass for all positions first, then a
# populated pass after enumerating outputs -- empty alone is not a failure.
# * layer-shell arms barriers PER OUTPUT, not just the desktop's outer rim,
# so an interior monitor edge is a valid crossing point.
# * [authorized_fingerprints] is keyed by FINGERPRINT: "<sha256>" = "<desc>".
# Reversed is a valid non-empty table, so paired() below CANNOT detect it;
# auth then fails silently and looks exactly like a dropped packet.
# * capture_backend/position/activate_on_startup/fingerprints all survive the
# rewrite; only comments are lost.
# * The daemon runs as the lan-mouse.service USER UNIT. Do NOT start it from
# autostart.lua's o.launch_on_start(): that sends stdout AND stderr to
# /dev/null, so every pairing failure is invisible. Cost: hours of
# diagnosing from a stale pre-reboot log file while the live daemon logged
# nowhere. `omarchy-handoff log` reads the journal for this reason.
# * The log level env var is LAN_MOUSE_LOG_LEVEL, NOT RUST_LOG. RUST_LOG is
# silently ignored, so it looks like debug logging "doesn't work".
# * Transport is DTLS over UDP (webrtc-dtls), not QUIC. The error
# "Alert is Fatal or Close Notify" (= ErrAlertFatalOrClose) means the REMOTE
# sent a fatal alert, i.e. the PEER REJECTED OUR CERT. Something answered,
# so it is a TRUST failure, never a firewall. Contrast "Connection timed
# out" = nothing answered at all (firewall, or the source-address trap
# below).
# * The fingerprint lan-mouse compares is sha256 of the DER certificate --
# exactly `openssl x509 -noout -fingerprint -sha256`. Not the PEM file hash,
# not the pubkey hash.
# * SOURCE-ADDRESS TRAP (two NICs on one subnet): the daemon listens on
# 0.0.0.0 but does not set IP_PKTINFO, so replies take the kernel's
# route-chosen source. With the dock up, `ip route get <peer>` prefers the
# dock NIC, so a peer connecting to our Wi-Fi address gets replies from the
# dock address and drops them as an unknown 4-tuple -> "Connection timed
# out" on that address only, while ICMP to it works fine. Undocked it
# inverts and works. Listing both peer-side IPs is correct; the unusable
# one just costs a 5s timeout per attempt.
# Thin wrapper over lan-mouse. lan-mouse's own config.toml is the single source
# of truth -- this script only drives its CLI so the edge and pairing are
# changeable without hand-editing TOML, and so a keybind has something to call.
set -euo pipefail
CONFIG="${CONFIG:-$HOME/.config/lan-mouse/config.toml}"
CERT="$HOME/.config/lan-mouse/lan-mouse.pem"
SOCKET="${XDG_RUNTIME_DIR:-/run/user/$UID}/lan-mouse-socket.sock"
LOG="${XDG_STATE_HOME:-$HOME/.local/state}/lan-mouse.log"
notify() { omarchy-notification-send -g "󰌌" "Handoff" "$1" 2>/dev/null || echo "$1"; }
die() { echo "omarchy-handoff: $1" >&2; exit 1; }
# The daemon owns the socket; every cli call is a no-op without it.
require_daemon() {
[[ -S $SOCKET ]] || die "lan-mouse daemon is not running (try: omarchy-handoff restart)"
}
# Resolve a client's numeric id from its hostname, since lan-mouse addresses
# clients by an index that shifts as entries are added or removed.
client_id() {
local host="${1:-}"
require_daemon
if [[ -n $host ]]; then
lan-mouse cli list | sed -n "s/^id \([0-9]\+\): ${host}:.*/\1/p" | head -n1
else
lan-mouse cli list | sed -n 's/^id \([0-9]\+\):.*/\1/p' | head -n1
fi
}
first_host() {
require_daemon
lan-mouse cli list | sed -n 's/^id [0-9]\+: \([^:]*\):.*/\1/p' | head -n1
}
cmd_status() {
if [[ -S $SOCKET ]]; then
echo "daemon: running"
else
echo "daemon: stopped"
return 0
fi
echo "peers:"
lan-mouse cli list | sed 's/^/ /'
echo "fingerprint (this machine):"
echo " $(cmd_fingerprint)"
}
cmd_fingerprint() {
[[ -f $CERT ]] || die "no certificate yet at $CERT (start the daemon once)"
# MUST be lowercase. lan-mouse compares with a plain HashMap key lookup
# (src/listen.rs: authorized.contains_key(&fingerprints[0])) against
# crypto::generate_fingerprint(), which formats "{x:02x}" and .to_lowercase().
# openssl prints UPPERCASE, so passing it through unchanged silently never
# matches -- that was the real cause of the dead crossing on 2026-10-10.
openssl x509 -in "$CERT" -noout -fingerprint -sha256 | cut -d= -f2 | tr "A-Z" "a-z"
}
# Activation is what actually gates input crossing the edge, so on/off/toggle
# map to activate/deactivate rather than stopping the daemon.
is_active() {
local id="$1"
lan-mouse cli list | grep -q "^id ${id}:.*active: true"
}
# Arming the edge with no authorized peer key is the one way this locks up a
# machine: input gets captured, the peer rejects it unauthenticated, and the
# keyboard looks dead. Refuse unless pairing is mutual, or --force is given.
# Parse the actual TOML table rather than slicing text: a line-range slice from
# the header to EOF also counts keys belonging to any table emitted AFTER
# [authorized_fingerprints], and `lan-mouse cli save-config` -- not us -- chooses
# that order. An empty fingerprint table followed by any populated table made the
# text version return true and arm an unpaired machine, i.e. it failed OPEN into
# exactly the lockup this guard exists to prevent.
# A malformed or unreadable config counts as 0, so it fails CLOSED.
paired() {
[[ -f $CONFIG ]] || return 1
local n
n=$(python3 -I -c '
import sys, tomllib
try:
with open(sys.argv[1], "rb") as f:
print(len(tomllib.load(f).get("authorized_fingerprints", {})))
except (OSError, tomllib.TOMLDecodeError):
print(0)
' "$CONFIG")
(( n > 0 ))
}
cmd_on() {
local force=0
[[ ${1:-} == --force ]] && { force=1; shift; }
local id; id=$(client_id "${1:-}")
[[ -n $id ]] || die "no peer configured"
if (( ! force )) && ! paired; then
die "no peer fingerprint authorized -- refusing to capture input.
Crossing the edge now would forward this keyboard and mouse to a peer that
cannot accept it, and the machine would look frozen (release: Ctrl+Shift+Super+Alt).
Pair first: omarchy-handoff pair <name> <sha256>
Override: omarchy-handoff on --force"
fi
lan-mouse cli activate "$id"
notify "Handoff enabled"
}
cmd_off() { local id; id=$(client_id "${1:-}"); [[ -n $id ]] || die "no peer configured"; lan-mouse cli deactivate "$id"; notify "Handoff disabled"; }
cmd_toggle() {
local id; id=$(client_id "${1:-}")
[[ -n $id ]] || die "no peer configured (try: omarchy-handoff peer <host> <ip>)"
if is_active "$id"; then cmd_off "${1:-}"; else cmd_on "${1:-}"; fi
}
# The whole point of the wrapper: the physical side the other laptop sits on is
# one value, not a code change.
cmd_position() {
local edge="${1:-}" id
case "$edge" in
left|right|top|bottom) ;;
*) die "position must be one of: left right top bottom" ;;
esac
id=$(client_id "${2:-}")
[[ -n $id ]] || die "no peer configured"
lan-mouse cli set-position "$id" "$edge"
lan-mouse cli save-config
notify "Peer is now on the $edge"
}
cmd_peer() {
local host="${1:-}" ip="${2:-}" edge="${3:-left}"
[[ -n $host && -n $ip ]] || die "usage: omarchy-handoff peer <hostname> <ip>[,<ip>...] [edge]"
require_daemon
local existing; existing=$(client_id "$host")
[[ -n $existing ]] && lan-mouse cli remove-client "$existing"
lan-mouse cli add-client --hostname "$host" --ips "$ip" --port 4242
cmd_position "$edge" "$host"
}
cmd_pair() {
local desc="${1:-}" fp="${2:-}"
# Normalise case: a fingerprint copied from openssl or a chat message is
# usually uppercase, and lan-mouse's comparison is case-SENSITIVE.
fp=$(tr "A-Z" "a-z" <<<"$fp")
[[ -n $desc && -n $fp ]] || die "usage: omarchy-handoff pair <description> <sha256-fingerprint>"
require_daemon
lan-mouse cli authorize-key "$desc" "$fp"
lan-mouse cli save-config
notify "Authorized $desc"
}
cmd_fixcase() {
[[ -f $CONFIG ]] || die "no config at $CONFIG"
python3 -I -c "
import re, sys
path = sys.argv[1]
with open(path) as f:
s = f.read()
pat = re.compile(\"\\\"([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){31})\\\"\\\\s*=\")
n = sum(1 for m in pat.finditer(s) if any(c.isupper() for c in m.group(1)))
s2 = pat.sub(lambda m: chr(34) + m.group(1).lower() + chr(34) + \" =\", s)
if n:
with open(path, \"w\") as f:
f.write(s2)
print(n)
" "$CONFIG" | {
read -r n
if (( n > 0 )); then
echo "lowercased $n fingerprint(s); restart to load: omarchy-handoff restart"
else
echo "nothing to fix -- all fingerprints already lowercase"
fi
}
}
cmd_restart() {
# Prefer the user unit, matching cmd_log's assumption. pkill + relaunch races
# the unit's Restart=on-failure and can leave TWO daemons contending for UDP
# 4242 and the socket, so only fall back to a hand-start when no unit exists.
if systemctl --user --quiet is-enabled lan-mouse.service 2>/dev/null \
|| systemctl --user --quiet is-active lan-mouse.service 2>/dev/null; then
systemctl --user restart lan-mouse.service
else
pkill -x lan-mouse 2>/dev/null || true
mkdir -p "$(dirname "$LOG")"
# Keep the log. Discarding it cost real debugging time once: the portal
# backend's silent no-op is invisible without these lines.
if command -v uwsm-app >/dev/null; then
uwsm-app -- lan-mouse daemon >>"$LOG" 2>&1 &
else
setsid lan-mouse daemon >>"$LOG" 2>&1 &
fi
fi
sleep 2
[[ -S $SOCKET ]] || die "daemon failed to start (omarchy-handoff log)"
notify "Handoff daemon restarted"
}
# Show what the capture layer is actually doing -- the only way to tell a real
# barrier from a backend that selected cleanly and armed nothing.
cmd_log() {
# The daemon runs as the lan-mouse.service user unit, so its output is in the
# journal. It USED to be started by o.launch_on_start() in autostart.lua,
# which sent stdout AND stderr to /dev/null -- so for hours this printed a
# stale pre-reboot file while the live daemon logged nowhere at all. Read the
# journal first; fall back to $LOG only for a hand-started daemon.
if systemctl --user --quiet is-enabled lan-mouse.service 2>/dev/null \
|| systemctl --user --quiet is-active lan-mouse.service 2>/dev/null; then
if [[ ${1:-} == -f ]]; then
journalctl --user -u lan-mouse.service -o cat -f
else
journalctl --user -u lan-mouse.service -o cat --no-pager -n "${1:-40}"
fi
return
fi
[[ -f $LOG ]] || die "no journal unit and no log at $LOG (try: omarchy-handoff restart)"
echo "# NOTE: reading fallback file $LOG (lan-mouse.service is not active)" >&2
if [[ ${1:-} == -f ]]; then tail -f "$LOG"; else tail -n "${1:-40}" "$LOG"; fi
}
# Health check. Deliberately exits non-zero on failure: a check that can only
# report success is decoration. Scope is capture + trust-entry SHAPE -- it
# cannot prove the peer trusts our key, only that our own table isn't malformed.
# Pass a saved log as $1 to parse it instead of probing (used to test failures).
cmd_doctor() {
local src="${1:-}" rc=0 probe cb last
echo "== configured backend =="
cb=$(python3 -I -c '
import sys, tomllib
try:
with open(sys.argv[1], "rb") as f:
print(tomllib.load(f).get("capture_backend") or "")
except Exception:
print("")
' "$CONFIG")
if [[ -z $cb ]]; then
echo " (unset -> defaults to input-capture-portal)"
echo " WARNING: that default is a SILENT NO-OP on Hyprland 0.56.2 -- it"
echo " initializes cleanly and creates no barriers. Set capture_backend = \"layer-shell\"."
rc=1
elif [[ $cb != layer-shell ]]; then
echo " $cb"
echo " WARNING: only layer-shell is known to create real barriers here."
rc=1
else
echo " layer-shell (correct)"
fi
echo
echo "== trust entry shape =="
# Checks CASE as well as ordering. An uppercase key is a valid TOML table
# entry and the right digest, so nothing anywhere complains -- the daemon
# just never matches it and both ends send DTLS bad_certificate.
local badcase
badcase=$(python3 -I -c "
import sys, tomllib
try:
with open(sys.argv[1], \"rb\") as f:
fps = tomllib.load(f).get(\"authorized_fingerprints\", {})
except Exception:
fps = {}
print(sum(1 for k in fps if any(c.isupper() for c in k)))
" "$CONFIG" 2>/dev/null || echo 0)
if [[ ${badcase:-0} -gt 0 ]]; then
echo " FAILING -- ${badcase} fingerprint(s) contain UPPERCASE hex."
echo " lan-mouse compares the string verbatim and generates lowercase, so"
echo " an uppercase key never matches: the digest is right, the case is not,"
echo " nothing logs a complaint, and both ends send DTLS bad_certificate."
echo " Fix: omarchy-handoff fixcase"
rc=1
fi
# Catches a REVERSED entry, which paired() cannot: a backwards
# "description" = "<sha256>" line is a valid non-empty table, and auth then
# fails silently and looks exactly like a dropped packet.
python3 -I -c '
import sys, tomllib
try:
with open(sys.argv[1], "rb") as f:
fps = tomllib.load(f).get("authorized_fingerprints", {})
except Exception:
print(" could not parse config"); sys.exit(1)
if not fps:
print(" NO fingerprints authorized -- the peer cannot be trusted yet."); sys.exit(1)
bad = [k for k in fps if k.count(":") < 16]
for k, v in fps.items():
label = "order OK " if k.count(":") >= 16 else "BACKWARDS"
print(f" {label} {k[:24]}... = {v!r}")
if bad:
print(" ERROR: key must be the FINGERPRINT, value the description.")
print(" Fix: lan-mouse cli remove-authorized-key <desc>, then")
print(" lan-mouse cli authorize-key <desc> <sha256>")
sys.exit(1)
' "$CONFIG" || rc=1
echo
echo "== peer reachability =="
# Added after both machines spent hours green on doctor while a firewall ate
# every inbound packet. Capture and trust can both be perfect and nothing
# crosses. This reads the DAEMON'S OWN connect outcomes -- ground truth from
# the real protocol, not a synthetic probe that proves nothing for UDP.
local pip pport ufwline verdict
pip=$(python3 -I -c "
import sys, tomllib
try:
with open(sys.argv[1], \"rb\") as f:
cs = tomllib.load(f).get(\"clients\", [])
ips = cs[0].get(\"ips\", []) if cs else []
print(ips[0] if ips else \"\")
except Exception:
print(\"\")
" "$CONFIG" 2>/dev/null || true)
pport=$(python3 -I -c "
import sys, tomllib
try:
with open(sys.argv[1], \"rb\") as f:
d = tomllib.load(f)
print(d.get(\"clients\", [{}])[0].get(\"port\") or d.get(\"port\") or 4242)
except Exception:
print(4242)
" "$CONFIG" 2>/dev/null || true)
if [[ -z $pip ]]; then
echo " no peer ip configured -- nothing to reach."
rc=1
else
echo " peer: ${pip}:${pport}"
if ping -c1 -W2 -q "$pip" >/dev/null 2>&1; then
echo " ICMP: host is up"
else
echo " ICMP: no reply -- peer is off, asleep, or on another network."
echo " (Everything below will fail for that reason alone.)"
rc=1
fi
# Our own inbound half. This is the check that was missing entirely.
if command -v ufw >/dev/null && sudo -n ufw status >/dev/null 2>&1; then
ufwline=$(sudo -n ufw status 2>/dev/null | grep -E "^${pport}/udp" || true)
if [[ -n $ufwline ]]; then
echo " our ufw: ALLOWS ${pport}/udp -- ${ufwline}"
else
echo " our ufw: NO rule for ${pport}/udp. If the default is deny incoming,"
echo " the peer's handshake never reaches our daemon and it looks like a"
echo " silent drop. Check: sudo ufw status verbose"
rc=1
fi
else
echo " our ufw: not checked (needs passwordless sudo); verify by hand."
fi
# What did the real protocol actually do most recently?
verdict=$(cmd_log 400 2>/dev/null | grep -aE "connect|Alert|connection" | tail -n1 || true)
if [[ -z $verdict ]]; then
echo " daemon log: no connection attempt recorded yet."
echo " (Cross the edge once, then re-run -- the attempt is what tells us"
echo " whether this is a firewall, a trust, or a routing problem.)"
else
echo " last connect line: ${verdict#*] }"
case $verdict in
*"Alert is Fatal or Close Notify"*)
echo " => the PEER ACTIVELY REJECTED our certificate (DTLS fatal alert)."
echo " Not a firewall: something answered. The peer does not trust"
echo " our fingerprint. Ours is:"
echo " $(cmd_fingerprint 2>/dev/null || echo "<no cert>")"
echo " The peer must authorize exactly that, keyed BY FINGERPRINT."
rc=1 ;;
*"Connection timed out"*)
echo " => NOTHING ANSWERED. Either the peer's firewall is dropping"
echo " ${pport}/udp, or our reply left via a different source address"
echo " than the peer sent to (two NICs on one subnet does this --"
echo " check: ip route get ${pip})."
rc=1 ;;
*"connection established"*|*"connected to"*)
echo " => transport is UP." ;;
esac
fi
fi
echo
echo "== barrier probe =="
if [[ -n $src ]]; then
probe=$(cat "$src")
else
# test-capture runs until killed, so timeout's 124 is expected, not a failure.
probe=$(timeout 8 lan-mouse --capture-backend "${cb:-layer-shell}" test-capture 2>&1 || true)
fi
if ! grep -q "adding capture" <<<"$probe"; then
echo " NO BARRIERS CREATED -- capture is dead."
echo " Full probe output:"
sed 's/^/ /' <<<"$probe"
rc=1
else
# test-capture probes ALL FOUR positions and the registration order is NOT
# stable, so a bare `| tail -n1` returns whichever edge happened to land
# last -- not ours. Observed this report "left" and "bottom" on consecutive
# runs from an unchanged config. Validate only OUR configured edge.
# Within that edge, the populated list grows as each output registers, so
# the LAST line for it is the authoritative one.
local pos
pos=$(python3 -I -c '
import sys, tomllib
try:
with open(sys.argv[1], "rb") as f:
cs = tomllib.load(f).get("clients", [])
print(cs[0].get("position", "") if cs else "")
except Exception:
print("")
' "$CONFIG")
if [[ -z $pos ]]; then
echo " cannot determine our configured edge from $CONFIG"
rc=1
else
echo " our configured edge: $pos"
last=$(grep "adding capture for position ${pos} " <<<"$probe" | tail -n1 || true)
if [[ -z $last ]]; then
echo " FAILING -- no barrier was ever created for \"$pos\"."
echo " (Other edges may have armed; that does not help us.)"
rc=1
else
echo " last line for $pos: ${last#*] }"
if [[ $last == *"using outputs: []"* ]]; then
echo " FAILING -- the final pass for $pos lists no outputs."
echo " (An empty pass BEFORE populated ones is normal and transient.)"
rc=1
else
echo " OK -- barrier is real on the output(s) above, so $pos can fire."
fi
fi
fi
fi
echo
if (( rc == 0 )); then
echo "RESULT: OK"
else
echo "RESULT: PROBLEMS FOUND (see above)"
fi
echo " Scope: capture barriers, reachability, and the shape of OUR OWN"
echo " trust entry. It cannot prove the peer trusts our key unless the"
echo " daemon has actually attempted a connection -- cross the edge once."
return $rc
}
case "${1:-status}" in
status) cmd_status ;;
toggle) shift; cmd_toggle "$@" ;;
on|enable) shift; cmd_on "$@" ;;
off|disable) shift; cmd_off "$@" ;;
position) shift; cmd_position "$@" ;;
peer) shift; cmd_peer "$@" ;;
fingerprint) cmd_fingerprint ;;
pair) shift; cmd_pair "$@" ;;
restart) cmd_restart ;;
log) shift; cmd_log "$@" ;;
fixcase) shift; cmd_fixcase "$@" ;;
doctor) shift; cmd_doctor "$@" ;;
*) die "unknown command '${1}'" ;;
esac