# MindAttic.Psst
Stop babysitting your terminal. Psst taps you on the shoulder the moment a long-running command finishes — a sound at your desk, a text on your phone.
Wrap any command. Walk away. Get pinged when it's done.
psst -- dotnet test psst -- npm run build psst -- terraform apply When the command exits, Psst plays a short attention-getter clip locally and sends you an SMS with the command name, exit status, and elapsed time — whether it succeeded or failed.
Why Psst #
- Reclaim your focus. Kick off a 12-minute build, switch to your inbox, get pinged when it's actually done. No more flicking back to the terminal every thirty seconds.
- Know without looking. The SMS tells you what ran, whether it passed, and how long it took. Stay in the meeting; glance at your phone.
- Works with anything. If it runs in a shell, Psst can wrap it — builds, tests, deploys, migrations, long
curls, ML training runs. - No daemon, no service. Just a single CLI. Nothing in the background, nothing listening on a port, nothing to babysit.
- SMS that actually arrives. Twilio first for real carrier delivery, with an email-to-SMS gateway fallback for zero-cost setups.
- Credentials stay yours. Secrets live outside the repo via the shared
MindAttic.Vaultchain — User Secrets,%APPDATA%, or environment variables. Never a.envchecked into source control.
CLI at a glance #
psst -- <command> [args...] Run a command. Play Psst + SMS when it exits. psst test [message] Fire a notification right now. psst ping Show which SMS transports are configured. psst sound Just play the Psst sound. psst contacts [list|add|rm] Manage the contact book. psst sms [flags] <to> <message...> Send a one-off SMS (see "Repeat & schedule" below). psst scheduled [list|cancel|clear] Inspect / cancel pending scheduled sends. psst pending Alias for `psst scheduled`. SMS is delivered via Twilio (preferred) with an email-to-SMS gateway fallback. Credentials are resolved through the shared MindAttic.Vault configuration chain (User Secrets / environment variables / optional appsettings.json).
Repeat & schedule #
The sms subcommand accepts three optional flags that let you drip a message at a cadence, defer it to a specific time, or both. Flags may appear anywhere in the sms arg list — before the recipient, after the message, mixed with each other.
| Flag | Alias | Argument | Meaning |
|---|---|---|---|
--repeat | — | positive integer | Send the message N times total. Default 1. |
--interval | --every | duration | Delay between repeats. Required whenever --repeat > 1. |
--schedule | --start | time-of-day | Defer the first send to local wall-clock time T (next occurrence) via Windows Task Scheduler. |
Duration format (--interval / --every) #
A non-negative integer followed by a unit suffix. Suffix is case-insensitive; a bare integer is treated as seconds.
| Form | Meaning | Examples |
|---|---|---|
Ns | seconds | 30s, 90s |
Nm | minutes | 5m, 30m |
Nh | hours | 2h, 12h |
Nd | days | 1d, 7d |
N | seconds (default) | 1800 |
Decimals (1.5h) are rejected — keep it integer-valued. Negatives are rejected.
Time format (--schedule / --start) #
A wall-clock time in the local timezone. Always resolves to the next future occurrence — if the time has already passed today, the schedule rolls forward to tomorrow.
| Form | Meaning | Examples |
|---|---|---|
| 12-hour with marker | hour:minute, am/pm | 10:30am, 2:30pm, 10:30 AM |
| 24-hour | hour:minute, no marker | 10:30, 22:30, 23:59 |
| Whole-hour shortcut | hour + am/pm only | 10am, 2pm |
Examples #
# Single send (no flags — runs in-process and returns immediately). psst sms jordan "MFE." # Twelve sends, five minutes apart. Detaches to Task Scheduler so the # shell isn't tied up for the whole hour. See "Implicit --schedule now". psst sms jordan "MFE." --repeat 12 --every 5m # Single send deferred to 10:30am local (today, or tomorrow if past 10:30). psst sms jordan "good morning" --schedule 10:30am # Defer to 9:00am, then ping five times one minute apart. psst sms jordan "standup" --start 9:00am --repeat 5 --every 1m Implicit --schedule now #
Whenever you pass --interval (or its alias --every) without an explicit --schedule / --start, Psst infers --schedule now for you. "Now" rounds up to the next whole-minute boundary, because schtasks /ST only supports minute precision; a small cushion is added when you're within 5 seconds of the boundary so the registration doesn't race the trigger.
The practical effect: a long drip loop hands itself off to Windows Task Scheduler instead of blocking your shell. You get your prompt back immediately, and the loop runs in a detached psst.exe child process spawned by Task Scheduler.
# These two are equivalent. psst sms jordan "ping" --repeat 12 --every 5m psst sms jordan "ping" --repeat 12 --every 5m --schedule now # (illustrative) How --schedule is implemented #
Under the hood, --schedule (and its alias --start):
- Resolves the time to a concrete local
DateTimeusing next-occurrence semantics. - Writes a small launcher
.cmdfile to%LOCALAPPDATA%\MindAttic\Psst\scheduled\<id>.cmdthat:- invokes
psst.exe sms …with the original argv minus--schedule(so the deferred run doesn't recursively re-schedule) and with--repeat/--intervalpreserved; - then runs
schtasks /Delete /TN <task-name> /Fand removes the JSON sidecar — so successful runs leave nothing pending behind.
- invokes
- Writes a JSON sidecar
%LOCALAPPDATA%\MindAttic\Psst\scheduled\<id>.jsonwith the recipient, message, repeat, and interval values, used bypsst scheduledto render a meaningful listing. - Calls
schtasks.exe /Create /SC ONCE /TN MindAttic.Psst.<id> /TR <launcher> /SD <date> /ST <time> /F.
Note:
schtasks /Z(auto-delete after run) is intentionally not used here — it requires anEndBoundarythat Windows doesn't synthesize from a bare/SC ONCE. The launcher self-deletes instead, achieving the same effect with zero edge cases.
Inspecting & cancelling scheduled sends #
psst scheduled # list all pending Psst tasks (alias: psst pending) psst scheduled list # same as above psst scheduled cancel <task-name> # delete one task + its launcher + sidecar (alias: rm, delete) psst scheduled clear # cancel every pending Psst task in one go Listing reads the actual Task Scheduler state (via schtasks /Query /FO CSV /V) and enriches each row with the JSON sidecar, so the table shows recipient, message preview, and repeat/interval at a glance. Already-fired tasks don't appear — they self-deleted on completion.
Example:
PS> psst scheduled Pending Psst tasks (2): ⏰ 2026-05-22 12:53 MindAttic.Psst.82894a30e2ad → jordan (12088996244): "deploy finished" ⏰ 2026-05-23 09:00 MindAttic.Psst.f73f70ff1e97 → jordan (12088996244): "standup reminder" ↻ 5 sends every 1m Cancel one: psst scheduled cancel <task-name> Cancel all: psst scheduled clear If you prefer the raw Windows tools, both still work:
Get-ScheduledTask -TaskName 'MindAttic.Psst.*' | Format-Table TaskName, State, ` @{N='NextRun';E={(Get-ScheduledTaskInfo $_).NextRunTime}} schtasks /Query /TN MindAttic.Psst.* # tab-complete task name first schtasks /Delete /TN <task-name> /F # cancel one Setting up Twilio #
Twilio is the primary SMS transport. You need three values: an Account SID, an Auth Token, and a Twilio phone number to send from.
1. Create a Twilio account #
- Go to https://www.twilio.com/try-twilio and sign up.
- Verify the email address and your personal mobile number — Twilio uses the mobile number both for 2FA on the console and as the only allowed destination for trial accounts (see "Trial accounts" below).
- After verification you land on the Twilio Console dashboard.
2. Buy (or pick) a phone number #
- In the Console, open Phone Numbers → Manage → Buy a number.
- Filter by country and tick SMS in the Capabilities column.
- Choose a number and click Buy (trial accounts get one number for free using trial credits).
- The number you bought — in E.164 format like
+15555550100— is yourtwilio:from.
3. Copy the Account SID and Auth Token #
- From the Console home page, scroll to Account Info.
- The Account SID starts with
AC…— this istwilio:accountSid. - Click Show next to Auth Token and copy it — this is
twilio:authToken. Treat it like a password; rotating it invalidates any client that uses the old value.
4. Trial accounts — verify destination numbers #
Trial accounts can only send SMS to verified numbers.
- Open Phone Numbers → Manage → Verified Caller IDs.
- Add and verify each number that will receive Psst notifications.
- Upgrade to a paid account when you need to send to arbitrary numbers.
5. Wire the credentials into Psst #
Psst reads from several sources, lowest → highest precedence:
| Source | Path | Notes |
|---|---|---|
.env fallback | %APPDATA%\MindAttic\Psst\.env | KEY=VALUE; outside the repo |
appsettings.json | .\appsettings.json (CWD) | optional, legacy |
| settings.json | %APPDATA%\MindAttic\Psst\settings.json | primary, outside the repo |
| User Secrets | per-user store, id mindattic-vault-shared | dev convenience |
| Environment variables | MindAttic__Vault__Notifications__* | CI / containers override |
Pick whichever feels right. The two file-based locations under %APPDATA% keep credentials out of the source tree without needing the dotnet user-secrets tooling.
Option A — settings.json (recommended) #
Create %APPDATA%\MindAttic\Psst\settings.json:
{ "MindAttic": { "Vault": { "Notifications": { "twilio": { "accountSid": "AC...", "authToken": "...", "from": "+15555550100" }, "to": "+15555550101" } } } } Option B — .env (fallback) #
Create %APPDATA%\MindAttic\Psst\.env (keys use __ between segments):
MindAttic__Vault__Notifications__twilio__accountSid=AC... MindAttic__Vault__Notifications__twilio__authToken=... MindAttic__Vault__Notifications__twilio__from=+15555550100 MindAttic__Vault__Notifications__to=+15555550101 Anything set in settings.json overrides the same key in .env.
Option C — User Secrets #
dotnet user-secrets set "MindAttic:Vault:Notifications:twilio:accountSid" "AC..." ` --id mindattic-vault-shared dotnet user-secrets set "MindAttic:Vault:Notifications:twilio:authToken" "..." ` --id mindattic-vault-shared dotnet user-secrets set "MindAttic:Vault:Notifications:twilio:from" "+15555550100" ` --id mindattic-vault-shared dotnet user-secrets set "MindAttic:Vault:Notifications:to" "+15555550101" ` --id mindattic-vault-shared Option D — Environment variables #
$env:MindAttic__Vault__Notifications__twilio__accountSid = "AC..." $env:MindAttic__Vault__Notifications__twilio__authToken = "..." $env:MindAttic__Vault__Notifications__twilio__from = "+15555550100" $env:MindAttic__Vault__Notifications__to = "+15555550101" 6. Verify it works #
psst ping # lists each source path and whether it was found psst test # sends a real SMS — check your phone Rotating or revoking a leaked Auth Token #
If you commit your Auth Token by accident, treat it as compromised:
- In the Twilio Console, open Account → API keys & tokens.
- Under Live Credentials, click Reset next to Auth Token (or create a new Standard API Key and migrate to using SID/Secret pairs, which can be revoked individually).
- Update the value everywhere it's wired up: User Secrets on each dev machine, CI secrets, server env vars.
- Audit recent message logs (Monitor → Logs → Messaging) for unexpected sends before assuming nothing went out under your account.
The same flow applies if a laptop is lost or a contributor leaves.
Notes on US A2P 10DLC #
If your from number is a US 10-digit long code, US carriers require registration under the A2P 10DLC program for non-trivial message volume. Unregistered traffic is heavily throttled and may be blocked outright. For personal notifier use this rarely matters, but if you start sending dozens of messages a day, register the number in the Twilio Console under Messaging → Regulatory compliance.
Email-to-SMS fallback (optional) #
Most US carriers expose a per-number email address that delivers as SMS. Examples: 5555550100@vtext.com (Verizon), 5555550100@txt.att.net (AT&T), 5555550100@tmomail.net (T-Mobile). Deliverability varies; this is the fallback when Twilio isn't configured.
Add an email block to %APPDATA%\MindAttic\Psst\settings.json:
{ "MindAttic": { "Vault": { "Notifications": { "email": { "smtpHost": "smtp.gmail.com", "smtpPort": 587, "username": "you@gmail.com", "password": "app-password", "from": "you@gmail.com" }, "toEmail": "5555550100@vtext.com" } } } } For Gmail you must use an app password (Google Account → Security → 2-Step Verification → App passwords), not your account password.