until — stop for now, pick it back up in this session
The case this exists for: a 5-hour session limit lands mid-task. The usual options are both bad — babysit the clock and come back to type “continue” at the reset, or let the agent poll “is it time yet?” and spend what quota is left on the polling. Instead, stop for now and arm a shell-side clock watcher that costs zero tokens while it waits, then wakes this same session at the reset time with a self-contained “go” line. The context is still loaded, so the work just continues.
It covers the plain timed case too: run X at 9pm, run X in 30 minutes, or queue a few jobs across tonight spaced apart.
$ARGUMENTS carries the when and the what: a time or delay, plus the work to run
when it arrives. It may also carry --caffeinate, which explicitly opts into holding
the Mac awake until the work fires. Remove that flag before parsing the time and payload.
Caffeination is off by default; never infer consent from the delay length, an overnight
target, or the human walking away.
Codex: if that token is not substituted, take the when and the payload from the text following the skill name in the current request.
This follows the patient-waiting ladder with the clock as the watched condition. That
skill owns the ladder and the hard rules; read it before arming anything.
Three tempting wrong answers
- Shell to a headless
claude.nohup claude -p "/review-pr …" &works and is wrong: it runs in a context-free throwaway session with no human in the loop, so nothing that happens is visible or steerable.atis not a fallback either — on macOSatrunships unloaded. /schedule,RemoteTrigger, cloud routines. They spawn an isolated cloud session with its own checkout: not this session, not this context, not this machine.ScheduleWakeup. Model-in-the-loop polling — a full-context turn per wake to ask whether it’s time yet.patient-waitingforbids it for exactly this.
Also not send-at (cmux-cli), the closest relative and the inverse: that one delivers a
prompt into another cmux surface at a target time and is bound to cmux ancestry.
until targets self, needs no cmux, and runs the payload rather than typing it
somewhere.
When they push for a cloud routine
“Make it a cloud routine so it survives my laptop closing” is a fair ask, and the answer is a priced tradeoff, not a refusal. Say both halves:
- What the cloud buys: it fires whether or not this machine or this session is alive.
- What it costs: a fresh isolated session with its own checkout. Everything this session knows is gone unless it is inlined into the routine prompt — the pasted docs, the specific worries, the standing rules about what may not be published — and nobody is in the loop at fire time to enforce them.
If the payload’s value is this session’s context, or a human at fire time, the cloud version is not the same job. Offer the choice; don’t silently re-tool. Usually three options: keep the watcher (accepting that a sleeping machine moves the fire to whenever it wakes), move to a cloud routine with the context inlined, or keep the watcher as primary and add a context-free cloud backstop. Inlining context into a cloud prompt externalizes it — name anything internal in that list and let the human decide, rather than pasting it on their behalf.
Mechanism (Claude Code)
A backgrounded Bash task running a shell loop that polls the wall clock. Arm it with
run_in_background: true; the harness invokes the model only when the task emits its
completion line, so the wait costs zero tokens and has no 30-minute cap.
TARGET=$(date -j -f "%Y-%m-%d %H:%M:%S" "2026-09-09 21:05:00" +%s)
while [ "$(date +%s)" -lt "$TARGET" ]; do sleep 30; done
echo "FIRE at $(date '+%-I:%M%p') — run /review-pr on https://github.com/OWNER/REPO/pull/701 now"
Six things there are deliberate:
- Epoch comparison in a loop, not one long
sleep. While the machine is asleep the whole shell loop is frozen, exactly as a singlesleepis — no form of this wakes a sleeping Mac, and a target that passes during system sleep does not fire on time. What differs is what happens on wake:sleep 11280counts monotonic time, so it resumes with its full remaining duration still to burn and fires late by however long the machine was asleep, while comparingdate +%sto a fixed target re-reads the wall clock and fires within one poll interval of the wake. Say that out loud whenever the horizon is long enough that the machine will plausibly sleep first — an overnight target, or any target the human is walking away from — and explain that--caffeinateis available (see Keeping the machine awake). Do not enable it unless requested. date -j -fis the BSD/macOS form. GNUdate -dfails here; on a GNU box it is the other way round, so checkunamebefore pasting either.- The loop exits after exactly one
echo. Exit ends the watch, so exactly one notification fires.while truewould stay armed after firing. - The emitted line is a self-contained instruction to your future self — it names the skill or command and the full target (URL, path, ticket id). The notification arrives bare, with none of this reasoning attached, so anything the payload needs has to be inside the line.
- The task emits; it does not do the work. A payload that is pure shell is tempting to run inside the loop — and then the fire is silent: no model turn happens, no session learns it fired, and nobody can report it or recover if it didn’t. Put the work in the emitted line and let the woken session do it. Doing it in the task is defensible only when a human reads the artifact directly and no one needs to be told; say so when you arm it that way.
- Do not replace it with a long-lived
Monitor. Claude Code 2.1.272 acceptspersistent: truebut does not honor it: the task still has a 30-minute cap. A successful tool call therefore does not prove persistence. If the background task is reaped and 30 minutes or less remain, aMonitorcan cover the remainder; otherwise say that no reliable in-session watcher remains and ask the human to nudge you.
Parsing <when>
Resolve <when> to an explicit calendar timestamp before arming anything, then run one
of these to sanity-check that the target lands where you think it does and to get the
human-readable fire time you are about to quote back — each is written to start with
date so the Bash(date *) grant actually covers what gets executed.
# absolute, same day: "9:05pm" / "21:05"
date -j -f "%Y-%m-%d %H:%M:%S" "$(date +%Y-%m-%d) 21:05:00" +%s
# absolute with an explicit date: "2026-09-10 09:00"
date -j -f "%Y-%m-%d %H:%M:%S" "2026-09-10 09:00:00" +%s
# relative: "in 20 minutes" / "in 2h"
date -v+20M +%s
These calls are a check, not a value to paste. For Claude Code, the epoch gets computed inside the
background task, the way the Mechanism block does it: the inline date -j -f
expression re-derives the local offset at arm time, so a re-arm — or a copy of the
command into another session — can’t fire against a stale number.
An absolute time already past today rolls to tomorrow — put tomorrow’s explicit date in the inline expression rather than adding 86400 to a number you carried by hand. Say so when you do it, and say the horizon out loud: an overnight target means the session, the machine, and the machine’s wakefulness all have to survive until then. If the human wants overnight, that is the moment to tell them this can’t guarantee it.
Keeping the machine awake
Nothing here wakes a sleeping Mac. By default, arm only the watcher and explain that a
sleeping machine delays the fire until it wakes. Hold the machine awake only when the
invocation includes the standalone --caffeinate flag. When present, remove the flag
from the time/payload input, arm the watcher first, then run this as a separate
backgrounded Bash call:
caffeinate -ims -t 12000
-t takes seconds: seconds-until-target plus about 600 of margin, so the assertion
outlives the fire and the payload’s first minutes. -i blocks idle sleep, -m keeps the
disk from idle-sleeping, -s holds the system awake while on AC power.
Keep caffeinate out of the watcher command string. Nesting the watcher inside
caffeinate … sh -c '…' puts the fire line in two layers of single quotes, and payloads
carry apostrophes and quoted glob args — that is the most likely way this watcher gets
armed broken, and it fails at fire time, hours later, silently.
caffeinate cannot override a lid close on a MacBook: a shut lid sleeps the machine
regardless of any assertion held. Say that when you arm it — the lid has to stay open.
Say this when you arm it (Claude Code)
- The background task dies with the session and may be reaped on handoff. If it is gone at fire time, nothing fires and there is no fallback or catch-up.
- The background task ID, so the human can cancel with
TaskStop. It exists only once the call returns, so it goes in the message after arming — relay it there; never invent one, and never drop it because the arming message came first. - The exact target time and the exact payload, in the words the notification will carry.
On fire
Run the payload. The notification is the go signal — reporting that it arrived and stopping is the failure mode this skill exists to prevent.
Resolve the payload’s skill or command name against the skill list for the current working directory rather than guessing it. Project skills can be exposed under a prefixed name from a subdirectory and a bare name from the repo root, so the name that worked when you armed the watcher is not automatically the name that works now.
Queueing several (Claude Code)
Up to about three: one background task each. Each exits after its own fire and each notification is unambiguous. Simplest thing that works.
More than that: one watcher holding a tab-separated epoch<TAB>payload schedule table,
emitting a line per due row and exiting once the last has fired. The table is the one
place literal epochs belong — the heredoc is quoted so payload apostrophes and $ are
safe, which also means it can’t re-derive a date expression. Read each number off the
date check at arm time.
while IFS=$'\t' read -r ts payload; do
while [ "$(date +%s)" -lt "$ts" ]; do sleep 30; done
echo "FIRE — $payload"
done <<'ROWS'
1757466300 run /review-pr on https://github.com/OWNER/REPO/pull/701 now
1757469900 run /review-pr on https://github.com/OWNER/REPO/pull/702 now
ROWS
Space heavy payloads apart. A multi-agent review is a heavy job and two firing on top of each other interleave badly; 20 minutes is a reasonable gap for review runs, not a universal number. Size the gap to how long one payload actually occupies the session.
Worked example — resuming after a session limit resets
A budget watcher (or the harness) reports the 5-hour session limit is close and names the reset time. The work in flight is a multi-step review that is not finished.
- Arm first, then spend what’s left. Pause at the warning, not at exhaustion — once a request is actually rate limited there is no turn left to arm anything. So the watcher goes up while quota remains, and only the leftover margin goes to finishing the step in flight. If the step won’t fit in that margin, stop mid-step and say which step and how far in; the fire line carries the resumption point either way.
- Resolve the reset time and arm one background task whose emitted line names the resumption
point, not just “continue”:
FIRE — quota refilled, resume the review of https://github.com/OWNER/REPO/pull/701 at the security pass (step 3 of 5) now. Back it with acaffeinatewindow only if the invocation includes--caffeinate. - Report the task ID the call returned, the exact reset time, and that the watcher dies with the session.
- Spend the remaining margin on the step in flight, then stop.
- On the notification, keep going. No resume note is needed — this is the same session and its context is still loaded. A resume note is for a pause that ends in a fresh session, which is what a context-window pause needs and this is not.
The harness can queue task-completion events while the session is rate-limited, but the
watcher itself is only as durable as its background task. Do not turn the 2026-09-10
rate-limit smoke of Monitor delivery into a persistence claim: later testing on
Claude Code 2.1.272 showed that persistent: true was accepted but ignored.
Worked example — a queue of PR reviews
The human hands over three PRs and the times they want them reviewed, and wants the reviews run here so they can watch and steer before anything is published.
- Resolve each target time; confirm the review command’s name for this cwd.
- Arm one background task per PR (three is inside the one-per-job range), each emitting
FIRE — run /review-pr on <full URL> now. - Report the three task IDs the calls returned, the three fire times, and the fact that all three die with the session.
- On each notification, run the review for that URL. Don’t batch it with a later one that hasn’t fired.
Codex — detached timer, same-thread queue
Codex CLI 0.160.1 was verified with `codex queue –thread