Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,13 @@ jobs:
id: sum
run: |
cd src-tauri/target/release/bundle/dmg
# Stable-named copy so the landing page can point at
# /releases/latest/download/Clockwork_aarch64.dmg and never go stale.
# The versioned file stays: the Homebrew cask pins its sha256 to it.
cp Clockwork_*_aarch64.dmg Clockwork_aarch64.dmg
shasum -a 256 *.dmg > checksums-sha256.txt
cat checksums-sha256.txt
echo "dmg=$(ls *.dmg)" >> $GITHUB_OUTPUT
echo "dmg=$(ls Clockwork_[0-9]*_aarch64.dmg)" >> $GITHUB_OUTPUT

- name: Create GitHub Release + upload assets (REST, no gh)
if: startsWith(github.ref, 'refs/tags/')
Expand All @@ -98,7 +102,7 @@ jobs:
\`\`\`

### Install
1. Download Clockwork_${VER}_aarch64.dmg (Apple Silicon)
1. Download Clockwork_${VER}_aarch64.dmg (Apple Silicon; Clockwork_aarch64.dmg is the same file under a stable name)
2. Open the DMG and drag Clockwork to Applications
3. Launch — pair with your local daemon using ~/.clockwork/api-token

Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ REPEAT Make it weekly. Search your retained run history.
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)
- 🧱 **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 `PreToolUse` hook, 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,
Expand Down Expand Up @@ -239,11 +242,19 @@ Full list: [docs/SHORTCUTS.md](docs/SHORTCUTS.md)
## ⚖️ Security Model

- **Isolation:** each run gets a fresh git worktree + branch cut from base;
macOS Seatbelt (`sandbox-exec`) profile restricts writes to that worktree
- **Credential hygiene:** sanitized child environment; deny-list blocks reads
of `.ssh`, `.aws`, `.gnupg`, Keychains; secret masking in reports
- **Approvals:** sensitive tool calls pause the run; ~2-minute decision window,
then fail-safe auto-deny (unattended mode) — recorded for audit either way
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_SOCK` and 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 — see `docs/security.md` for 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
Expand Down
21 changes: 21 additions & 0 deletions decisions/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,24 @@ ADR-style, append-only. Format: Decision → Context → Alternatives rejected
**Alternatives rejected:** Electron fallback (heavier; not needed since nothing blocks on native yet); blocking all UI work on Rust installation (schedule risk for zero architectural delta).
**Why:** The UI code is identical under both wrappers (fetch/EventSource only); Tauri contributes the window chrome, tray, autostart, updater — all packaging-time concerns.
**Consequence:** Token handshake currently manual (file read) until Tauri injects it at spawn; tray/menubar surfaces land with the Tauri step; stack #4 FullCalendar replaced by a purpose-built week/month grid matching designs/DESIGN.md (bundle size + we control booking UX end-to-end; FullCalendar's recurring-event model fights our occurrence-ledger source of truth).

## ADR-034 — Supersedes ADR-020: CLI keep-alive approvals via a loopback permission bridge; Seatbelt sandbox wired into production for every engine; S-39 narrowed to preserve interrupted worktrees
**Decision:** (a) The Claude CLI engine asks Clockwork before every gated tool call through `--permission-prompt-tool`, served by an HTTP MCP server hosted inside `runner-child` (`packages/runner/src/permission-server.ts`, zero deps). The hold is bounded by the run's remaining wall-clock budget, not a fixed window; `MCP_TOOL_TIMEOUT` and the per-server `timeout` are set to the same bound. (b) Every engine spawn — Claude, Codex, OpenCode, Hermes, and the BYOK agent's bash — is routed through `applySandbox()`; `runner-child` builds the spec with `buildSandboxSpec()`. The only way out is `CW_SANDBOX=off`, which is logged, written to the safety journal (`sandbox_disabled`), and stamped on the report (`sandboxed:false`). (c) Profile v2 admits the CLI's per-cwd work dir (`/tmp/claude-<uid>/<slug>`, pre-created) and its cwd-tracking file (`regex ^/private/tmp/claude-[0-9a-f]+-cwd$`), plus `SandboxSpec.writeRegexes` for engine staging files. (d) `runner-child` scrubs `CW_BYOK_KEY`/`CW_BYOK_BASE_URL` from its own env after reading them; the BYOK bash gets `buildRunEnv()` + the sandbox wrap. (e) S-39 prunes a worktree only when the run ended cleanly (`completed`/non-crash `failed`) AND the worktree is clean with no git operation in flight; otherwise it is preserved and the report says so (`worktreeState`).
**Context:** 2026-09-05, the day after the Product Hunt launch. Three public statements did not match the code: approvals "hold until you approve" (they never fired — `onPermissionRequest` was called only by `mock-runner.ts`); "runs execute inside a macOS Seatbelt sandbox" (`new ClaudeCliRunner()` at `runner-child.ts:67` passed no spec; the other engines had no hook; T-111's "productionized" meant generator + tests only); "the worktree is preserved" after a timeout (`removeWorktree` ran `git worktree remove --force` + `rmSync` whenever the agent committed nothing, including on `timed_out`). ADR-020's premise — `--permission-prompt-tool` absent — was verified against 2.1.238 and is false on the installed 2.1.261; ADR-020 itself required this re-run. Evidence: `spikes/reports/T007-engine-contract-matrix-2.1.261.md` (real runs: 100s hold honoured inside the sandbox; `/tmp` write and `~/.zsh_history` read denied with a live shell; OpenCode verified; Hermes 0.21.0 resolves `write_file` against `$HOME` even outside the sandbox — pre-existing, now loud; Codex unverifiable here due to a local `config.toml` error).
**Alternatives rejected:** stdio MCP bridge (the CLI spawns it INSIDE the sandbox with stdio owned by the CLI → needs a side channel and a profile allow for it); extending the fixed 120s window (still a lie about "holds"); `--strict-mcp-config` (drops the repo's own `.mcp.json` servers — behaviour change for existing tasks); falling back to an unsandboxed spawn when the profile is refused (fail-open); a blanket allow on `/tmp` for the CLI's cwd file (the regex admits one filename and was tested against near-misses).
**Why:** The launch's credibility argument is "check it rather than believe it". Every one of the three gaps was a missing call site in front of working, tested code — so the fix is wiring plus guards that read the sources (`runner-env-wiring.test.ts` "sandbox wiring") so the call sites cannot silently disappear again.
**Consequence:** T-003/T-201 unblocked for the CLI engine; the composer may offer `default` permission mode meaningfully. `docs/security.md`, README, `plan/STATUS.md` T-111 corrected to describe what ships. Open, surfaced not decided: the run inherits `HOME`, so `~/.claude/settings.json` `permissions.allow` rules pre-empt the prompt tool — `--setting-sources` (2.1.261) can pin what an unattended run loads. ADR-026…033 are cited in code but never written here; this entry takes 034 to avoid collision. Re-run the matrix on every observed CLI/engine version change (unchanged from ADR-020).

## ADR-035 — The policy floor as a fail-closed PreToolUse hook; exactly one Seatbelt layer; engine cwd pinning

**Decision:** (a) Every Claude CLI run injects a `PreToolUse` hook (matcher `Bash`) via `--settings`; the generated hook (`packages/runner/src/floor-hook.ts`) POSTs `{tool_name, tool_input}` to the permission bridge's `/floor` route, the supervisor runs `evaluateCommand`, and a floor hit exits 2 (CLI refuses the call with the reason). Every error path exits 2 — bridge unreachable, malformed input, timeout, watchdog — so the hook is fail-closed and a CLI contract change breaks runs loudly. Floor hits are sent to the daemon (`{t:'floor'}`), recorded as `policy_deny` events and `deny_list_hit` journal entries. (b) Codex runs with `-s danger-full-access` when Clockwork's Seatbelt profile is on and `workspace-write` only under `CW_SANDBOX=off`. (c) `HermesRunner` sets `TERMINAL_CWD` to the worktree.

**Context:** 2026-09-05/06. The ADR-034 bridge only sees tool calls the CLI chooses to gate; under `acceptEdits` on CLI 2.1.261 `git push --force origin main` ran with no prompt (production probe, `permission_denials: []`). macOS refuses to apply codex's own profile inside ours (`sandbox_apply: Operation not permitted` under any `(deny default)` outer profile; bisected every allow, only `(allow default)` nests). hermes 0.21.0's oneshot (`-z`) path never applies `--in`, so writes resolved against `$HOME` — silently before the sandbox, loudly (EPERM) after it.

**Alternatives rejected:** running unattended tasks in `default` mode (every call prompts — turns a 2am run into a wall of asks); injecting deny-list patterns as CLI `permissions.deny` rules (pattern dialect differs from ours, two sources of truth, still mode-dependent); a hook that imports the runner's dist (a packaging path baked into a run; an exec failure would be fail-open); exempting codex from the sandbox (loses credential-read denial and the "every engine wrapped" guard); making nesting work (no outer allow unblocks it).

**Why:** hooks fire in every permission mode, so coverage no longer depends on the CLI's gating heuristics; evaluation stays in the supervisor with the real deny-list; the hook has zero imports beyond `node:http`, so packaging cannot break it. One containment layer that is ours is simpler to reason about than two that fight. Measured cost: ~60 ms per Bash call (Node start + one loopback round trip, median of ten).

**Consequence:** docs/security.md "Known gap" closed with dated evidence; codex users lose codex's own shell-command network block (network is allowed under Clockwork's profile for every engine — documented). Open: the run inherits `HOME`, so a developer's `permissions.allow` rules and `SessionStart`/`SessionEnd` hooks apply unattended (`--setting-sources` pinning is a product decision); repo-declared MCP servers that run shell are not matched by the `Bash` matcher; the bridge has no per-run bearer token yet; LICENSE §12's audit set does not yet list `permission-server.ts` or `floor-hook.ts`.

**Review findings folded in (2026-09-06, Opus read-only pass, both reproduced on this machine):** (1) the `--settings` payload must pin `disableAllHooks: false` — the CLI honours that switch from a repo's own `.claude/settings.json`, and without the pin one committed key disabled the hook while the report still said `sandboxed: true`; CLI-flag settings outrank project settings, so the pin wins (unit-tested in `floor-hook.test.ts`). (2) The BYOK provider key must not travel in the child's environment at all: macOS keeps a process's exec-time env readable via `sysctl KERN_PROCARGS2`, the profile must allow `sysctl-read` (Node needs it), and a sandboxed agent read the key out of a sibling `runner-child` after it had been deleted from `process.env`. The credential now arrives over the daemon⇄child stdin channel as a `credential` message; the env never contains it.
2 changes: 1 addition & 1 deletion docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ trust this specific binary, so verify it first:

```bash
# 1. Check the hash matches the published one
shasum -a 256 ~/Downloads/Clockwork_0.4.0_aarch64.dmg
shasum -a 256 ~/Downloads/Clockwork_0.5.0_aarch64.dmg
curl -s https://clockwork.vmoksh-shah179.workers.dev/downloads/checksums-sha256.txt
```

Expand Down
Loading
Loading