Book β Run β Review β Repeat
Schedule recurring AI agent jobs on a real calendar. Clockwork executes them unattended in isolated, sandboxed worktrees β and files a report you can actually read.
Website Β· Download Β· Contact Β· Agent Library Β· Agent Workforce Β· Providers Β· Security
You already pay for a coding agent. It idles 18+ hours a day.
Recurring agent work today lives in crontabs, shell scripts, CI pipelines, and sticky notes. Clockwork gives that work a home on a real calendar:
| Without Clockwork | With Clockwork |
|---|---|
| Cron + terminal tabs | Month calendar with every job visible |
| Hope the script worked | Report with branch, diffstat, cost, transcript |
| Unbounded token spend | USD soft cap, plus hard turn / wall-clock caps |
| Agent has your whole disk | Per-run OS-sandboxed git worktree |
| Find last Tuesday's run: scrollback | Full-text search across retained history |
| The agent's work is never graded | An explicit verdict per run β accept, reject, or accept with a note the next occurrence reads |
| Month calendar β the default view | Task composer |
|---|---|
![]() |
![]() |
| Run report | Command palette (βK) |
|---|---|
![]() |
![]() |
BOOK Pick an agent profile, repo, budget, and time. One-off or recurring.
β
RUN Your own CLI engine executes unattended inside an OS-sandboxed
git worktree. Never touches main. SSH keys unreadable.
β
REVIEW A human-readable report lands in your inbox β what it did,
what it skipped and why, what it cost. You record a verdict.
β
REPEAT Make it weekly. Search your retained run history.
- π A real calendar β month/week views, recurrence (RRULE + cron), missed-run policies, per-repo mutex, queue with reasons
- π€ Human + agent time β subscribe your personal calendar via a read-only
ICS URL, or import a local
.icsfile as a dated snapshot you re-import yourself. Recurring external events render as a single(recurring)base occurrence β Clockwork does not expand an external RRULE (packages/daemon/src/ics.ts) - π Provider freedom β Claude Code, Codex CLI, OpenCode, and Hermes Agent; switch per task without rebuilding anything
- π€ 13 production-grade agent profiles β Dependency Surgeon, Test Doctor, Security Auditor, Code Reviewer and more, each with mission, constraints, safety rails, and an output contract
- π‘ Human-in-the-loop approvals β risky actions pause the run and ask you;
unanswered asks fail safe (never silently approved), and notify you when a run
is waiting. The macOS notification is unconditional β no per-task setting gates
it. Telegram, Slack and email are all configured from the app β credentials in
Settings, per-task fields in the composer β and all three now receive the
approval request as well as the run report, through one fan-out that
serves both directions (
packages/daemon/src/delivery-dispatch.ts). The decision is not symmetric, and the screens say so. You answer in Clockwork, or from the Telegram message itself (packages/daemon/src/telegram-approvals.ts), which carries approve/deny buttons behind an inbound poller. Slack and email can only point you at the Inbox β Slack because its interactivity POSTs the click to a public HTTPS URL and this daemon binds loopback only, so a button there would be a control that silently does nothing. The generic HMAC-signed outbound webhook has its signing secret in Settings, but its per-task URL is set through the API only; there is no field for it in the composer - π§± Policy floor in every mode β force-pushes to protected branches and
package publishing are refused before they run on the Claude engine, even when
the CLI would not have asked (a
PreToolUsehook, fail-closed, ~60 ms per call) - π Searchable execution history β FTS across every report and transcript; βK command palette everywhere
- π° Budget enforcement by the supervisor β USD soft cap, turn limits, wall-clock timeouts enforced outside the model
- π Agent chains β sequence agents (scan β fix β test β PR); each stage
waits for its upstream and receives its report via
{{previous.report}} - π Bring your own key β OpenAI-compatible API providers (Anthropic, OpenAI, Google, OpenRouter, xAI, Mistral, DeepSeek, Ollama, custom gateways) with keys sealed in the macOS Keychain, connection validation, and clear separation from CLI-subscription billing
- π Governance, with the seams shown β the policy engine really does run on
every task create, task edit and webhook fire (engine allow-lists and a per-run
cost ceiling,
packages/daemon/src/policy-engine.ts), and control-plane mutations really are appended to the audit log. But reading either back is a paid route:GET /policiesandGET /auditanswer 402 on the free tier, which is the tier every install runs at today, and neither has a screen. The retention sweep runs on a cadence and at startup (default 90 days / 1000 runs per task,packages/daemon/src/retention-audit.ts), and Settings βΊ Retention now shows what the next sweep will delete and when, with both numbers editable behind a confirmation that names them.requireApprovalOverUsdis stored and validated but nothing consumes it yet - π Watch it work β a live output tail while a run executes, with cost and turns ticking beside it, and a catch-up read so a tab opened mid-run is not blank. An approval raised while you are watching is answerable in the same view
- π The morning after β the Inbox opens on a digest when runs are unread: what needs you, what did not finish cleanly, what it spent, how many branches are waiting. Counted the way the Analytics tab counts, deliberately, so the two cannot disagree
- π A menu-bar item β next run, running count, and a badge you cannot miss when an approval is waiting, while the window is shut. Closing the window hides it; quitting still leaves the scheduler running, because launchd owns the daemon
- π§° Five jobs you can book without typing a prompt β Monday dependency triage, flaky-test sweep, Friday docs-drift check, morning repo-health digest, pre-release changelog draft. Export any task as a template and share it; import puts it through the same security preview a stranger's file gets
- π Cost & reliability analytics β spend by task/provider/day with
optimization suggestions that surface money-burning failures. Runs still in
flight count as runs and as spend, are reported separately, and are excluded
from every rate denominator and from average duration
(
packages/daemon/test/analytics.test.ts) - π Local-first β SQLite in
~/.clockwork, loopback-only API, no account, no cloud, no telemetry. The other side of that: runs happen only while your Mac is awake. Clockwork holds it awake across a run's window when you are on mains power, but it cannot wake a sleeping machine β see Known limits. There is no Clockwork-hosted runner and none is planned: it would need your provider key, which would negate the Keychain promise - π§βπΌ Agent workforce (12 features) β plan-then-execute approval gates, shift-handoff memory, office hours, sentinelβworker pairs, repo-shipped job offers, run verdicts, an earned-autonomy ladder, self-healing diagnostics, agent-proposed calendar events, timesheets, scorecards, and a portable proof-of-work export. All twelve have a screen; three of them actually gate something. Full table below
Event triggers β webhooks and GitHub events start tasks; see docs/triggers.md.
Clockwork drives the CLIs you already have β no API keys required.
| Provider | Auth | Status |
|---|---|---|
| Claude Code (default) | Your Claude subscription login | β |
| Codex CLI | Your ChatGPT/Codex login | β |
| OpenCode | Its own configured model | β |
| Hermes Agent (Nous Research) | Your Hermes-configured provider/model | β |
Detection is automatic (Settings β Providers): if the CLI is installed and
logged in, it appears with its version and a health check. Select the engine
per task in the composer β same task schema regardless of provider.
How provider execution works
Every provider implements the same AgentRunner contract (packages/shared/src/runner.ts):
spawn in the run's worktree, stream progress logs over SSE to the UI, enforce
budget bounds at supervisor level, map exits to failure classes
(auth / capacity / timeout / budget), and return a structured outcome that
becomes the report. Hermes runs via hermes -z one-shot mode with
--usage-file cost telemetry; Claude Code via claude -p --output-format stream-json; Codex and OpenCode via their native headless modes.
Profiles are production-grade operating contracts, not name stickers. Each one defines mission, constraints, hard safety rules, and an output contract.
| Engineering | Operations & Docs |
|---|---|
| Dep Surgeon β patch/minor bumps proven by tests; majors get triage notes, never blind upgrades | CI Investigator β infra-flake vs regression triage from real logs |
| Test Doctor β flaky vs broken classification, minimal fixes, never weakens assertions | Repo Health Monitor β morning digest: stale branches, drift, advisories |
| Bug Hunter β evidence-first root cause before any fix | Docs Scribe β fix documentation drift from code evidence |
| Code Reviewer β read-only, severity-rated findings with file:line evidence | Changelog Writer β entries derived from actual diffs, never invented |
| Refactoring Engineer β behavior-preserving, tests green at every step | |
| Performance Engineer β measure baseline β change one thing β re-measure | |
| Security Auditor β report-only defensive scan; secrets masked | |
| Release Engineer β version/changelog/build readiness checks |
Twelve specialists, plus a Generalist for work that does not fit one β thirteen
seeded profiles in all (packages/daemon/src/profiles.ts +
packages/daemon/src/profile-library.ts).
Create your own in-app (Agents β New profile): pick skills, permission mode, budget defaults, and system prompt β bookable a minute later.
Twelve features that turn a calendar of scheduled runs into something closer to
a team you manage. Every one of them has a screen in this build, and each screen
declares its own location at the module scope of the file that mounts it
(packages/ui/src/components/featureSurfaces.ts) β so the capability matrix
behind Settings β "What does each plan include?" can tick a capability only
when a mounted component actually registered one, and the tick doubles as a link
that takes you there. Forgetting to register under-claims; it cannot over-claim.
Read the status column literally. It is the word packages/daemon/src/features.ts
carries, and that file defines the two values narrowly:
- enforced β daemon code outside the
/workforce/*routes refuses or defers a user action because of this feature. - available β you can reach it today, and it gates nothing.
Three qualify as enforced. The other nine are real features you can use; they just do not stand in anything's way.
| Feature | Where it lives | Status | What it does, and what it refuses |
|---|---|---|---|
| Plan β execute | Tasks βΊ Plan β execute |
enforced | One booking becomes two runs: a plan-mode run at an hour you pick, then an execute half created enabled=0 that stays that way. Approving the plan books the execute run directly rather than re-enabling the task. Refuses: while the pair is unapproved, POST /tasks/:id/run-now and a webhook fire on the execute half both return 409; PATCH /tasks/:id {"enabled":true} on an execute half returns 409 at any pair status, because re-enabling it would let a later plan run fire it through the chain with a plan nobody read (planExecuteGate, packages/daemon/src/api.ts). |
| Shift handoff | Inbox βΊ a runβs report |
available | A recurring task carries a memory across occurrences β what it tried, what blocked it, what to check next. One setup step is yours: the memory is injected only if the prompt contains the literal {{handoff.previous}}. A task that does not ask never gets it. |
| Office hours | Settings βΊ Office hours |
enforced | You declare the windows in which you can answer an approval. Defers, never cancels: a due fire is pushed to the next window and the occurrence is recorded deferred. It applies only to tasks whose profile carries may_require_approval, and no profile route sets that column β autonomy enrolment is the only writer, which makes it a three-step setup, not two. Off by default, and it fails open: a broken config, no matching window, or an unflagged profile all mean "fire on schedule". |
| Sentinel β worker | Tasks βΊ Sentinels |
available | A cheap, frequent check books the expensive run when it trips, through the same policy and trigger path a webhook fire uses. Every evaluation is written down β a non-trip, a cooldown, a disabled sentinel, and a policy refusal each leave a row with its reason. Refuses (422): a sentinel bound to a trigger that books the sentinel itself, which is an infinite loop. |
| Repo-shipped jobs | Tasks βΊ Repo jobs |
available | A repo can declare recommended jobs in .clockwork/jobs.json (or .yaml, through a restricted parser that adds no new dependency). Clockwork offers them and imports nothing on its own; import creates the task disabled, behind a security preview computed at discovery time. The job file cannot choose its own power or budget: import hardcodes acceptEdits, $2 / 50 turns / 1h and no profile, whatever the file asks for. The red-flag 422 on import is real but unreachable from discovery β see the F5 note in the guide before treating it as a defence. |
| Accept with a note | Inbox βΊ a runβs report |
available | The per-run verdict: accept, reject, or accept-with-a-note. Re-deciding updates the verdict instead of stacking a second one. A note is appended to that task's handoff memory as a human-authored entry, so your correction is what the next occurrence's agent reads. This is the acceptance signal the autonomy ladder, timesheets and scorecards all read. |
| Earned autonomy | Settings βΊ Earned autonomy |
enforced | Opt-in, and a rung is offered, never granted β a streak of accepted runs writes an offer row and nothing else; only your acceptance writes the new rung. Refuses (403 autonomy_rung_exceeded) at task create, task patch and the webhook fire path β in practice only when the profile sits at the bottom plan rung and the task asks for another mode. The top two rungs share the permission mode acceptEdits, so neither refuses anything; the acceptEdits β unattended step's whole effect is to clear the office-hours flag. Enrolling overwrites the profile's permission mode, and the app warns before it does. An unenrolled profile is unconstrained, on purpose. |
| Self-healing | Inbox βΊ Approvals (remediation proposals) |
available | After N consecutive failures (default 3) Clockwork books one diagnostic run with the failed transcripts as context, whose instruction is "propose exactly one change, change nothing". The output is an approval item. The agent never edits its own prompt or profile: inside this feature, apply() is the only writer of tasks.prompt / tasks.profile_id, and it runs only from your click on Apply change β never from inside the diagnostic run (packages/daemon/src/self-healing.ts). Elsewhere in the product, PATCH /tasks/:id can still write both columns; that is your edit, not the agent's. At most one diagnostic per failure streak; a failed diagnostic books no second one. |
| Proposed events | Inbox βΊ a runβs report (when it proposes events) |
available | A report may suggest calendar events, offered as a download. Clockwork never writes to your calendar β there is no write path in the module, and the ICS overlay stays read-only. Model output is untrusted, so the parse is bounded: β€20 suggestions, β€8 KB block, first block only, Clockwork assigns the .ics UID, control characters stripped, credentials masked, and a bad block costs the suggestions rather than the run (packages/runner/src/proposed-events-parse.ts). Nothing is injected into your prompts, so an agent that is never asked proposes nothing. |
| Timesheets | Analytics βΊ Timesheets |
available | Hours worked, dollars spent, outcomes accepted, and an effective hourly rate per profile over any range β against an optional rate for your own time. The accuracy caveat is shown unconditionally, because a run that was still active when the daemon restarted is closed out at the restart time and so counts the downtime as work; the per-row flag on top of that is best-effort. The rate is null, never Infinity or 0, when no hours were worked. |
| Performance reviews | Analytics βΊ Performance reviews |
available | Acceptance rate, failure rate and cost trend against the prior window β plain SQL, no model call. An unreviewed agent reads as "not yet reviewed", never as a 0% failure. /review-prompt returns the text of a prompt; writing the prose review means scheduling a task with it. No seeded reviewer profile does that for you. |
| Proof-of-work export | Inbox βΊ a runβs report |
available | One run's report as a single self-contained HTML file you host yourself β no script tags, no stylesheet links, no remote images, no telemetry pixel. Secrets are masked with no flag to turn it off, every interpolation is escaped, redactPaths strips repo/worktree/branch, the transcript is off by default, and every export is audited. |
Routes, refusal paths, per-feature tests and the measured numbers: docs/agent-workforce.md.
macOS 13+ (Apple silicon). Free during beta. The app carries the daemon and its own Node runtime β nothing else to install.
# 1. Verify the bytes BEFORE you trust them
shasum -a 256 ~/Downloads/Clockwork_*_aarch64.dmg
curl -s https://github.com/vimoxshah/clockwork/releases/latest/download/checksums-sha256.txt
# if the two do not match: stop, do not install, report it
# 2. Open the DMG, drag Clockwork to Applications, then clear quarantine
xattr -dr com.apple.quarantine /Applications/Clockwork.app
# 3. Launch
open -a ClockworkThat is the whole install (a 51 MB download; 192 MB once installed, most of
it the Node runtime). The app carries the daemon and its own Node, so
there is no checkout, no pnpm, no Node to install, and no token to paste. On
first launch Clockwork registers its background service with launchd, which is
what keeps scheduled runs firing after you close the window and after a reboot,
and pairs the window with the daemon's token itself. Move the app afterwards and
it re-points the service at the new location.
Two things it cannot bring with it, because they are yours and already logged
in: git, and at least one agent CLI (claude, codex, opencode or
hermes). clockworkd doctor names either one when it is missing.
Releases are unsigned. There is no Apple Developer certificate on this
project, so the release workflow ad-hoc signs the bundle
(.github/workflows/release.yml exports APPLE_SIGNING_IDENTITY="-" when no
certificate is configured) and nothing is notarized. macOS Gatekeeper will
refuse the app β often with a misleading "damaged" message β until you clear the
quarantine flag by hand. Clearing it tells your Mac you trust that specific
binary, which is exactly why the hash check comes first. Homebrew
(brew install --cask clockwork) verifies the SHA-256 for you but cannot skip
quarantine either. Full detail, including the Homebrew tap:
docs/install.md.
# 1. Clone and install
git clone https://github.com/vimoxshah/clockwork.git
cd clockwork
pnpm install
# 2. Build everything
pnpm build
# 3. Start the daemon (serves UI + API on 127.0.0.1:4747)
node packages/daemon/dist/main.js
# 4. Pair the UI
open http://127.0.0.1:4747
# Paste the token from:
cat ~/.clockwork/api-tokenPrerequisites:
- Node.js β₯ 22, pnpm β₯ 11 (
corepack enable). Runpnpm installwith the same Node you will start the daemon with:better-sqlite3is a native module compiled for one Node ABI, so switching versions afterwards (a Homebrew upgrade will do it) leaves the daemon unable to start, withNODE_MODULE_VERSIONin~/.clockwork/daemon.log.err.pnpm installagain to rebuild. - At least one provider CLI installed and logged in:
- git
Desktop app (Tauri)
pnpm tauri build # unsigned .app + DMG in src-tauri/target/release/bundle/tools/stage-bundle.mjs runs first and stages what the app needs to stand on
its own: the daemon's production tree into Contents/Resources/app (laid out
as a miniature of the repo, because main.js walks to ../migrations,
../../ui/dist and ../../../resources/skill-pack) and the Node running the
build into Contents/MacOS via externalBin, where Tauri signs it. That Node
is the one better-sqlite3 is compiled against in the same command, so the
NODE_MODULE_VERSION mismatch cannot happen in a released build.
The shell installs the LaunchAgent, re-points it when the app moves, restarts a
daemon left stale by an update, and pairs the token. When the daemon still does
not answer, the window shows daemon-down.html (packages/ui/public/) with the
cause this install actually has and one command to run, then navigates to the
real UI as soon as the port answers.
This build is unsigned and un-notarized; the release workflow only signs when Apple Developer credentials are present in CI, and they are not.
Run as a background service
# clockworkd entrypoint: install/uninstall/doctor
npx tsx packages/daemon/src/cli.ts install
clockworkd doctor # verifies PATH, providers, data dirOnly one daemon may hold the port; a second one exits with a diagnostic naming the version skew instead of crash-looping behind an older build. The app shows a restart banner when the running daemon and the one on disk disagree.
Open Clockwork and press Run a sample job now. That is the whole procedure.
Clockwork points the read-only Code Reviewer at the first git repository it can find β one you have booked work against before, one it cloned for you, or the first one the folder picker shows in your home folder β caps the run at $0.50, starts it immediately, and opens the live view so you watch it work. If it finds no repository it says so, names where it looked, and offers a small bundled snippet instead; it never guesses a folder.
That first run cannot change anything. It runs in permission mode plan,
inside a throwaway worktree, under a sandbox profile that mounts your
repository read-only β three separate reasons, none of them a paragraph in a
prompt.
Prefer to drive it yourself? + New task takes a name, a profile, a repo, a cap and a schedule β or start from one of five shipped templates. Either way the report lands in your Inbox with the branch, the diffstat and the cost. Record a verdict while you are there; several features read it.
| Keys | Action |
|---|---|
βK |
Command palette (navigate, themes, create) |
βN |
New task |
β1β5 |
Calendar / Inbox / Tasks / Agents / Analytics |
β, |
Settings |
/ |
Focus inbox search |
Full list: docs/SHORTCUTS.md
ββββββββββββββββ HTTP + SSE (loopback :4747, bearer token 0600)
β UI (React) ββββββββββββββββββββββββββββββ
ββββββββββββββββ β
Tauri shell loads the same URL β
β
ββββββββββββββββββββββββββββββββββββββββββββββΌββββ
β clockworkd (Fastify) β
β scheduler Β· queue + repo mutex Β· run manager β
β approvals Β· budgets Β· delivery Β· audit journalβ
β SQLite ~/.clockwork β
ββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β spawn per run
ββββββββββββΌββββββββββββ
β runner child processβ own pgid, Seatbelt profile
β AgentRunner contractβ Claude β Codex β OpenCode β Hermes
ββββββββββββββββββββββββ worktree-isolated git operations
- Monorepo:
packages/shared(schemas/contracts) Β·packages/runner(engine runners + sandboxing) Β·packages/daemon(API/scheduler/state) Β·packages/ui(React + Tailwind design system) - Deterministic tests:
CW_MOCK_STEP_MSmakes full-loop integration tests sample intermediate states without sleeps - A screen declares itself: a capability is ticked as reachable only when the
component that mounts it registers a surface at module scope
(
packages/ui/src/components/featureSurfaces.ts,packages/ui/src/components/LicenseCard.tsx). A feature that ships a daemon route with no screen, or an orphan component nobody imports, registers nothing β so the failure mode is a missing tick, not a false one
- Isolation: each run gets a fresh git worktree + branch cut from base;
a macOS Seatbelt (
sandbox-exec) profile restricts writes to that worktree, Clockwork-managed tool caches, and the engine's own state dirs β for every engine, including the BYOK agent's shell. Turning it off (CW_SANDBOX=off) is journaled and stamped on the report. - Credential hygiene: sanitized child environment (allowlist, so
SSH_AUTH_SOCKand provider keys never reach the agent); the sandbox denies reads of.ssh,.aws,.gnupg, gcloud, browser profiles, shell history; secret masking in reports. Keychain files stay readable β Claude Code needs its own OAuth item β seedocs/security.mdfor why. - Approvals (Claude engine): gated tool calls pause the run and hold until you answer or the run's wall-clock budget ends, then fail-safe deny β recorded for audit either way. Other engines have no permission hook; the sandbox is their containment.
- Budgets: USD soft cap + turn cap + wall-clock timeout enforced by the supervisor process, not by the model's self-restraint
- Local-only: daemon binds 127.0.0.1; bearer token file is 0600; no analytics, no account, no cloud component
Audit it yourself. packages/runner/src/{sandbox,deny-list,run-env,service-path}.ts
are the security boundary, dual-licensed under Apache-2.0 (LICENSE Β§12), and
packages/runner/test/ exercises them against a real sandbox-exec β the suite
writes a fake secret into ~/.ssh and ~/.aws, then runs cat inside the
sandbox and asserts it fails.
What that does not prove:
- That the DMG you downloaded was built from this source. Releases are built by GitHub Actions from this repository and the checksums are published, but reproducing the binary yourself is not yet supported.
- That Apple has checked anything. The build is not signed with an Apple Developer identity and not notarized β see Installation.
- That the sandbox alone stops ssh-agent forwarding. The Seatbelt profile permits
system-socket; that claim holds because of the run-env allowlist, not the sandbox.
Details: docs/security.md Β· docs/privacy.md
Open, reproducible, and written down here rather than discovered by you:
- A refused execute booking strands its pair. When you approve a plan, the
verdict commits first and the booking is attempted second. If the booking is
refused β the policy engine rejects the execute half, the execute task was
deleted between the plan run and your decision, or the booker throws β the pair
stays at status
approvedwith no execute run, and a second resolve answersalready_resolved, so nothing re-books it (packages/daemon/src/plan-execute.ts:327,:349). A policy refusal is audited (plan_execute.book_rejected) rather than silent. Run now on that half is allowed once the pair readsapproved, but that path does not bind the approved plan into the prompt, so it is not an equivalent recovery. (A paused daemon is not one of these cases: the run is queued and the pair reachesexecutednormally.) - The calendar latency ceiling is proven on one machine, not on the
acceptance machine. Every number was measured on an Apple M4; the
acceptance criterion names a base M1 Air, which has never been measured at
all. On the M4 the year-view median over 5,000 runs is
40.82β42.17ms across three full test runs on 2026-09-07, against a 500 ms
bound. Before the RRULE anchor fix the same
measurement ranged 349.59β684.26ms across ten runs on one laptop at one
commit: it met the bound in six and missed it in four, and the p95 was above
the ceiling in seven, decided by machine load rather than by code. That is
why the bench measures by default and only asserts under
CLOCKWORK_BENCH_ASSERT=1β the gate is about a wall-clock assertion deciding a build, not about the size of the margin. Full record:plan/STATUS.md(T-307) anddocs/architecture/scalability.md. - One recurrence shape still hangs, and the defect is upstream. An hourly
rule whose coarser
BYpart cannot be reached from its ownINTERVALgrid βFREQ=HOURLY;INTERVAL=2;BYHOUR=3, whose hours stay even β never terminates inside rrule 2.8.1's skip loop. It behaved that way before the anchor work and it behaves that way after, because the reachable residues depend only ongcd(INTERVAL, 24).guardSchedulerefuses exactly this shape when a task is saved, with anunreachableverdict, and a one-time sweep at daemon startup (sweepHazardousSchedules,packages/daemon/src/main.ts) runs the same check over every enabled recurring row saved before that guard existed: each one it refuses is disabled and gets an inbox item naming the rule and the fix, so a database carrying one boots into a stopped task rather than a wedged daemon. The tick path itself is still deliberately unguarded, and that is where the residue now sits β a hazardous rule written straight into SQLite while the daemon is running stays live until the next restart sweeps it. - One older capability still has no screen: container execution.
GET /targetsreports whether Docker is available and nothing dispatches a run to it β a probe, not a target, with nothing to set. It registers no surface, which is why the capability matrix leaves it unticked. The other three are no longer among them.DeliveryConfigcarries aquietHourskey ({ startHour, endHour }) and Settings has a Quiet hours card beside Office hours β and it works now: the deferral used to pre-claim the resume instant, which made the resume tick's own claim a no-op, so a deferred recurring schedule stuck at that instant and never fired again and a deferred one-shot was dropped outright (ADR-030). Retention has a card showing what the next sweep will delete and when, with both numbers editable behind a confirmation that names them β and its free-tier cap used to sit below the 90-day window every install ships with, soPUT /retentionrefused the value it had just handed you. An outbound webhook's URL is a field in the composer's delivery block. That URL is settable at task creation only:view()does not return a task's currentdeliveryandpatch()replaces it wholesale rather than merging, so editing one afterwards would drop the rest of that task's delivery config. A profile's delivery config is still API-only. - Keep-awake holds the Mac awake, but cannot wake it. The daemon arms a
macOS power assertion (
caffeinate) for a run's budgeted window and releases it afterwards. It declines on battery unless you setCLOCKWORK_KEEP_AWAKE_ON_BATTERY=1, and the OS still wins if you shut the lid. Nothing schedules a wake, so a machine already asleep at the fire time stays asleep, the run is handled by the missed-window policy, and there is no window to watch. When a run is open, the daemon samples its own window every 15s and the report says in words when the Mac slept for 60s or more during it β "This Mac slept for about 42 minutes during this run."sleptDuringRunMscarries the figure, and it is a floor: up to one 15s sample low. Two limits remain. Off macOS, and for a run recovered after a daemon restart, nobody watched, so that field andsleptThroughKeepAwakeare both absent rather thanfalseβ the report declines to answer instead of answering "no". And a sleep past 60s starves the heartbeat too, so the run can still finalize asrunner_crashed; the sleep sentence explains that report rather than preventing it. For genuinely unattended overnight work, use a machine that stays on.
- BYOK guide β connect Anthropic, OpenAI, Google, DeepSeek, Z.ai, and more; key storage, defaults, error decoding
- Install & security β verified checksums, what Clockwork can reach once installed, and why quarantine must be cleared by hand
- Troubleshooting β daemon, auth, scheduling, and license/plan problems
- Agent workforce guide β all twelve features: routes, what each one enforces vs. merely offers, and where it's tested
pnpm typecheck # shared + runner + daemon
pnpm lint # eslint
pnpm test # vitest β unit + integration incl. full-loop E2E
pnpm build # all workspace packages
# UI package only
pnpm --filter @clockwork/ui dev # vite (proxies nothing; use served app)
pnpm --filter @clockwork/ui buildThe e2e scripts under packages/ui/e2e/ are Playwright harnesses used during
development to verify the real served application end-to-end (calendar,
themes, palette, providers, ICS overlay, 1000-task benchmarks).
The docs are on the build. packages/daemon/test/claims-honesty.test.ts,
feature-honesty.test.ts and landing-honesty.test.ts read this README, the
landing page and the design docs, and fail the suite when the prose claims more
than the code delivers β a permanence claim retention would contradict, a cited
path that does not exist, an upgrade modal selling a feature that was deleted.
An overclaim here is a red build, not a marketing choice.
- Multi-provider execution (Claude/Codex/OpenCode/Hermes)
- BYOK API providers (8 kinds, Keychain-stored, validated)
- Command palette + shortcut registry
- Human calendar overlay β ICS by subscription URL and by local file import
- 1000-task scale verification
- Agent chains (chain-after + trigger states +
{{previous.report}}hand-off) - Docker execution target β runner module (
runInDocker,packages/runner/src/docker-runner.ts) and an availability probe (GET /targets) exist, but nothing wires a task run to it yet: no task-level target field, no run-manager dispatch, no UI selector - Governance: policy engine, audit log, retention, capability matrix (evaluation and audit-writing run on every install; reading policies or the audit log is a paid route and neither has a screen β see Highlights)
- Event triggers: webhook + GitHub sources fire tasks (HMAC-verified)
- Plan-then-execute approval gate + sentinelβworker trigger pairs +
repo-shipped job offers (
docs/agent-workforce.md) - Governance ladder: office-hours approval deferral, an earned-autonomy
ladder that only offers (never auto-grants) the next rung β of which
only the bottom rung refuses a task, see
docs/agent-workforce.mdβ self-healing diagnostics that only propose (never self-apply) a fix - Workforce analytics: shift-handoff memory, accept/reject-with-a-note, agent timesheets, performance scorecards, portable proof-of-work export, download-only agent-proposed calendar events
- A screen for all twelve agent-workforce features β Settings gains office hours and the autonomy ladder, Tasks gains planβexecute pairs, sentinels and repo-job import, Analytics gains timesheets and scorecards, the Inbox gains handoff memory, proposed events and proof-of-work export
- Self-declaring capability surfaces β a feature is ticked as reachable only
when the component that mounts it registers itself, so the plan matrix
cannot claim a screen that does not exist
(
packages/ui/src/components/featureSurfaces.ts) - Telegram delivery: bot credentials in Settings, chat id per task in the composer, and approve or deny a waiting run from the chat message
- Slack and email delivery β Slack posts Block Kit through an incoming webhook; email goes out over an SMTP client written on Node's standard library, with no dependency added. Both have Settings fields with a test-send and per-task composer fields, and both receive approval requests as well as run reports. Neither can take the decision β that still happens in the app or in Telegram
- Chaining v2 (fan-in/out DAGs)
- RRULE expansion for external calendars β
packages/daemon/src/ics.tsemits one(recurring)base occurrence per recurring event instead - Manual check-for-updates β a click, never a timer, from the menu bar and from Settings; the four outcomes are stated and a failed check never reports "up to date". No automatic or signed update path exists
- A live run tail, a morning digest, a menu-bar item, five bookable templates and template export
- An accessibility and keyboard sweep that runs in CI and can fail β both audits previously exited 0 unconditionally, and one was scoped to a CSS class the app had stopped using
- Per-day calendar aggregation and a bounded
/calendarpayload βGET /calendar?group=dayreturns one row per non-empty day with its outcome breakdown, both modes cap at 5,000 rows per collection, and every response reports the bound it applied and whether it hit it - Signed & notarized desktop builds β needs an Apple Developer
certificate ($99/yr); the release workflow already imports one when
APPLE_CERT_P12is present and ad-hoc signs when it is not - Kubernetes / cloud execution targets beyond Docker
- SSO / SCIM for enterprise deployments
Clockwork is proprietary software β see LICENSE.
- Personal, non-commercial use: free. Run it on your own machines for your own work.
- Commercial use (company-wide internal use, redistribution, or offering Clockwork-based functionality to others): requires a written Commercial License from the author.
- The repository is public so you can audit exactly what runs on your machine β that transparency is a feature, not an invitation to redistribute.
Third-party open-source components keep their own licenses; see NOTICE.



