@drew2 shipped in adom/hook 1.1.8 — and good news on the bootstrap wrinkle: it's already solved, so the generalization is smaller than you'd budgeted.
The bootstrap wrinkle is stale (I checked, not assumed)
lite/service images often lack the adom-wiki CLI itself, and there is no anonymous binary-download URL... A self-bootstrap story (bundled binary or a blessed public bootstrap endpoint) would make this genuinely zero-touch.
Both halves already exist:
- The blessed public bootstrap endpoint is live:
https://wiki.adom.inc/static/bootstrap.sh → 200, no auth. It does exactly the dance you'd want: dist-tags → /assets → pick the matching platform/arch → curl the binary → chmod +x → pkg install adom/core. It also treats the registry as the root of trust (refuses plaintext/non-Adom/known-dead mirrors, so a hijacked ADOM_WIKI_REGISTRY can't deliver a backdoored CLI).
- The anonymous binary download exists:
GET /download/adom/adom-wiki-cli/1.0.37/adom-wiki-cli-v1.0.37 → HTTP 200, 12,282,080 bytes, no Authorization header. Verified just now.
So zero-touch on a bare lite container is one line today:
curl -fsSL https://wiki.adom.inc/static/bootstrap.sh | bash
Which means session-monitor can drop the gzipped binary from its repo. Shipping a binary in a package repo is a second copy of the CLI that ages independently and has to be re-vendored on every release — the bootstrap endpoint gets you the current one, verified, for free. Same for self-update each cycle: pkg install adom/core already pulls the CLI (core depends on adom/adom-wiki-cli ^1.0.0).
The trigger — the part that was actually missing
You were right that the script was already the right shape: throttle + stamp, nothing about it assumes a prompt. The Claude-specificity was entirely in install.sh. 1.1.8 registers cron there:
*/30 * * * * bash ~/.adom/hooks/adom-core-update.sh
@reboot bash ~/.adom/hooks/adom-core-update.sh
One deliberate departure from your option 1: you suggested a flag or auto-detecting "no Claude Code here" and using cron instead of UserPromptSubmit. I wired cron unconditionally, in addition — the throttle stamp makes a double fire a no-op, so it's strictly additive, and it closes a gap auto-detect would leave open: an interactive box that sits idle for a day currently doesn't update either. No prompts, no updates — same rot, just a slower clock. Now it has a backstop. It also removes a branch that can guess wrong.
Idempotent (re-install doesn't duplicate the lines), and uninstall removes them rather than leaving cron firing at a deleted script. Verified on a real container: 0 entries → pkg update → 2 entries.
Daemon mode I'd skip, and I want to be honest about why rather than half-ship it: a loop needs a supervisor, and on an image with no cron and no systemd there isn't one except the app's own entrypoint. So cron is the supported path; if crontab is missing or crond isn't running, install now says so out loud and points at the escape hatch — have the app call ~/.adom/hooks/adom-core-update.sh on its own interval (it's throttled and idempotent, safe to call often). That's your session-monitor pattern, minus the vendored binary.
Observability without SSH
Two things landed for this:
- Under cron, stdout is meaningless (mail or
/dev/null), so the durable record is ~/.adom/last-update.status (running, or the last exit code) and ~/.adom/last-update.log. Those arrived in 1.1.7, which fixed something worth knowing about: the hook used to pipe the update to /dev/null and then print "updating in the background now" unconditionally — so a hard failure was indistinguishable from success. That masked a fleet-wide auto-update outage for four days (adom/core depended on the deleted adom/pup; every container's update 404'd silently). A failed update is now loud.
- Fleet-wide, no SSH at all: every container now reports its CLI version to the registry on update, so
GET /api/admin/clients?name=adom-wiki-cli&below=<ver> answers "is the headless fleet current?" directly. It fails closed — no data reads as UNKNOWN, never as "all good".
Closing this as shipped; reopen if a lite image you care about has no cron and the app-loop escape hatch doesn't fit.