A typed CLI that schedules WhatsApp medication and appointment reminders on top of OpenClaw's own cron engine, instead of reimplementing a scheduler. Every command executes with a strict argv array — never a shell string — and the data model has no field capable of storing a diagnosis or clinical detail. Built to demonstrate the judgment healthcare-adjacent automation actually requires: knowing exactly where the compliance boundary sits, and enforcing it in the schema, not just in a policy document.
| Metric | Value |
|---|---|
| Tool | remind — a Node.js/TypeScript CLI, 7 commands |
| Scheduling | OpenClaw's native cron engine — no custom scheduler built or maintained |
| Delivery | Real WhatsApp messages via openclaw message send |
| Storage | SQLite as a metadata cache — OpenClaw remains the source of truth for scheduling state |
| Command Execution | execFile/spawn argv arrays only — zero shell string interpolation, anywhere |
| Compliance | No PHI fields exist in the schema — only free-text label/message the user authors |
| Tests | Vitest — argv-safety suite covers quotes, semicolons, ampersands, pipes, Unicode, emoji |
| Status | All 7 commands live-verified against a real OpenClaw install and real WhatsApp delivery |
WhatsApp Reminder CLI is a reusable command-line tool that schedules medication and appointment reminders, delivered over WhatsApp, using OpenClaw's own cron engine for both scheduling and delivery. It does not reimplement a scheduler — it wraps openclaw cron and openclaw message send, and keeps a local SQLite store purely as a friendly, typed CRUD layer on top. It was scoped deliberately as a personal reminder utility, not a clinical system: no dosage validation, no adherence analytics, no diagnosis storage, no multi-patient or caregiver management.
All seven CLI commands, the OpenClaw cron wrapper, and the WhatsApp delivery path are real and were run end-to-end against a live OpenClaw install — including real reminder messages delivered to a real phone, shown in the screenshots below.
A "simple reminder tool" is a deceptively easy place to make three expensive mistakes, especially once the reminders are medication-related:
Building a custom cron/retry engine duplicates infrastructure that already exists, and introduces a second source of truth to keep in sync — a whole class of bugs a reliable platform already solved.
User-authored reminder text and phone numbers are exactly the kind of untrusted input that becomes dangerous the moment a tool concatenates them into a shell command.
"Just add a dosage field" or "just log whether they took it" are reasonable-sounding asks that quietly turn a to-do app into an unregulated clinical record system.
This project builds a reminder tool that leans on an existing, reliable scheduling engine, treats every user-supplied value as unsafe for shell interpretation, and keeps the compliance boundary enforced by the database schema itself — not by a policy someone has to remember to follow.
The CLI is a thin, typed layer over OpenClaw's own cron engine — OpenClaw is always the source of truth for scheduling state, and SQLite is only a local read cache for metadata the CLI needs to show back to the user:
| Layer | Job |
|---|---|
CLI (remind) | Parses add / list / show / edit / delete / pause / resume via Commander.js |
| OpenClaw Cron Engine | Source of truth — schedules, enables/disables, and tracks run history for every job |
| SQLite | Metadata cache only — label, message, recipient, schedule, and the linked openclaw_job_id |
openclaw message send | Delivery — runs directly as the cron job's command payload, no LLM in the send path |
Every mutating command follows one non-negotiable ordering rule: the openclaw cron call always executes first, and the local SQLite write only happens if that call succeeds. A failed OpenClaw call leaves the database exactly as it was and prints the underlying error verbatim — there is no such thing as a "draft" reminder row with no backing job.
| Layer | Technology |
|---|---|
| Runtime | Node.js + TypeScript |
| CLI Framework | Commander |
| Storage | better-sqlite3 — single-file, synchronous, zero external DB dependency |
| Testing | Vitest — DB layer, OpenClaw wrapper (mocked execFile/spawn), and CLI command tests |
Every mutating command executes the openclaw cron call first and only writes SQLite on success — insert for add, update for edit/pause/resume, delete for delete. A reminder row's existence is the claim that a matching OpenClaw job exists.
The OpenClaw wrapper invokes the binary via execFile/spawn with an argument array only — never exec() or string concatenation. Required unit tests assert this holds for reminder text and recipient values containing quotes, semicolons, ampersands, pipes, Unicode, and emoji.
OpenClaw's cron add exposes two payload flags: --command (genuinely shell-interpreted at fire time) and --command-argv (a literal JSON argv array, no shell). This tool always uses the latter — closing the injection surface at the Gateway's own execution step, not just at this tool's own invocation of openclaw.
Without --no-deliver, a job's delivery mode defaults to an "announce back to the session" fallback that has no session to announce to for a headless CLI-created job — surfacing as a confusing status: error even though the actual WhatsApp send succeeded. Found by inspecting a real job's run history against a live install, not assumed from documentation, then fixed by always passing --no-deliver.
A live failed call revealed openclaw cron add rejects --tz alongside --every outright. The SPEC's original assumption (always safe to pass) was corrected: --tz is now only sent for cron schedules or offset-less at datetimes, while the local timezone column still stores whatever the user supplied regardless.
The reminders table has exactly one free-text field for content (message) and no structured clinical fields — no dosage units, no ICD codes, no provider-as-entity. The compliance boundary holds because the database literally cannot store what it was never given a column for.
The SQLite file is chmod'd 0600 on first run, verified working on native Linux filesystems (ext4). On a Windows-drive/DrvFs mount (e.g. WSL's /mnt/c/...) without the metadata mount option, chmod succeeds without error but NTFS has no POSIX bits to persist, so the file stays 777 regardless — confirmed by a direct probe on the dev machine. Where this applies, the real safeguard is the no-PHI schema, not the file permission.
If the process is killed after a successful OpenClaw call but before the matching SQLite write completes, the DB can under-represent OpenClaw's actual state. No two-phase commit or reconciliation command was built for v1 — intentionally, since it's a single-user local CLI where "don't write the DB on failure" already covers the common case, and OpenClaw remains recoverable manually via openclaw cron list if the rare race is ever hit.
The tool confirms a reminder was sent — never whether medication was taken. Building "did they take it" tracking would cross from a personal to-do utility into clinical monitoring territory, which is explicitly out of scope for this project.
This is the kind of trade-off documentation a real engagement should produce — not just a working CLI, but a record of what's solid, what's a known limitation, and why, kept in the project's spec rather than silently worked around.
Stated plainly, because a tool that hides its posture is worth less than one that states it:
The README states the boundary directly: don't type diagnosis, dosage rationale, or other clinical detail into a reminder message — treat it like a personal to-do text, not a medical record. Encryption at rest (e.g. SQLCipher) is documented as a target, not built in v1, since it isn't needed while reminder text stays non-clinical — but it's the first thing that would need to change before this tool could ever hold anything more sensitive.
Every command below was run against a real OpenClaw install and produced a real WhatsApp delivery:
remind add arrives on WhatsApp exactly as authored, no paraphrasing.
openclaw cron get.
Stated deliberately, because a tool that hides its limitations is worth less than one that states them:
remind show confirms the job ran, not that a human saw it.exec()" — closing the injection surface at both this tool's invocation and the Gateway's own execution step, verified with a live probe job, not just read from documentation.--help output and real job runs, catching wrong assumptions (a missing --tz constraint, a wrong delivery-mode default) that documentation alone wouldn't have surfaced.Whether it's WhatsApp reminders, a scheduling layer over a platform you already trust, or any tool where the compliance boundary needs to be enforced by design rather than by policy, we're always interested in discussing privacy-first automation.