foldrun 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/foldrun.mjs CHANGED
@@ -38,6 +38,7 @@
38
38
  // foldrun eval new <name> a new eval in a deployed workspace
39
39
  // foldrun agent run <name> run one agent once on a platform, no flow
40
40
  // foldrun tool test <name> exercise one tool alone — no model, no run
41
+ // foldrun hook <verb> ls / add / test / rm — your own code at every tool call and step
41
42
  // foldrun deploy [dir] push this account — or one workspace — into an installation
42
43
  // foldrun pull bring the platform's account down into this folder
43
44
  // foldrun status what differs here from what is deployed — --platform [--history]: is the platform up
@@ -64,6 +65,11 @@ import { fileURLToPath } from "node:url";
64
65
  import fs from "node:fs";
65
66
  import path from "node:path";
66
67
  import process from "node:process";
68
+ import { guardTerminal } from "../src/terminal.mjs";
69
+
70
+ // Before anything prints: a platform's text never writes escape sequences to
71
+ // a person's terminal (src/terminal.mjs). Pipes and files are untouched.
72
+ guardTerminal();
67
73
 
68
74
  const HERE = path.dirname(fileURLToPath(import.meta.url));
69
75
 
@@ -117,6 +123,10 @@ const HELP = `foldrun — agents are just folders
117
123
  foldrun flow paste <flow> step markdown from stdin or --file in as new groups (--after <g>, default last) or beside one (--group <g>) — diff, checked, --dry-run
118
124
  foldrun flow draft "<what it should do>" Draft with AI on the platform (--to; --flow <name> redrafts one): shows the files and the check, saves on yes (--yes, --dry-run)
119
125
  foldrun tool test <name> exercise one tool alone, no model and no run (key=value args, --to)
126
+ foldrun hook ls the hooks in this workspace and the account library: on, tool, description — invalid ones with the reason (--json); --to <ws>: the DEPLOYED workspace's, who turns each on, and what they denied, rewrote or failed lately (--runs <n>, --all)
127
+ foldrun hook add <name> --on <event> a hook folder: hook.md and a run.mjs that allows everything, the contract in its comments (--tool <pattern>, --library, --to)
128
+ foldrun hook test <name> run one hook here on a sample event, as a step would: its answer and how long it took (--event '<json>'|@file; --to <workspace>: the deployed one, on the platform)
129
+ foldrun hook rm <name> delete a hook folder — says which agents still turn it on, asks (--yes, --library)
120
130
  foldrun extract [dir] move single-file script tools into folders (tool.md + run.*)
121
131
  foldrun run <target> run an agent or flow HERE (target: name, or flow:name) — on the platform: flow run / invoke a flow, agent run one agent
122
132
  foldrun eval [name] run one eval, or all of them (--to <workspace>: the deployed evals, run on the platform)
@@ -171,7 +181,7 @@ const HELP = `foldrun — agents are just folders
171
181
  foldrun history [path] every change to a deployed workspace, newest first (--id <revision> for its diff, --limit, --to)
172
182
  foldrun history restore <path> --id <revision> put one file back as that revision left it, as a new revision — diff, asks (--yes, --dry-run, --to)
173
183
  foldrun repo ls a workspace's branches and tags — also diff <branch>, deploy <ref>, merge <branch> (both ask first), rm-branch <branch>, mirror <git-url|off>, mirror-now (--to)
174
- foldrun restore <ws> --to <commit|time|3d> put a workspace's source back as it was then — shows the change, asks for the name typed back (--dry-run: the diff only, --yes)
184
+ foldrun restore <ws> --to <commit|time|age> put a workspace's source back as it was then — shows the change, asks for the name typed back (--dry-run: the diff only, --yes)
175
185
  foldrun backups how the account is backed up (encrypted?), when it last was, the snapshots it is in — also request --what runs|state|storage|everything --at "<when>" [--to <ws>] [--note]
176
186
  foldrun backups requests every restore asked of the platform team, and where each stands (--json)
177
187
  foldrun secrets set NAME store a secret (prompted, never echoed) — also ls, rm, status; --kind file|ssh|api|service-account|m2m for the other shapes
@@ -188,7 +198,7 @@ const HELP = `foldrun — agents are just folders
188
198
  foldrun workspaces show <name> what is deployed in one: its agents, flows and how each fires, its files by folder (--json)
189
199
  foldrun workspaces new <name> --platform make it on the platform: blank (an AGENTS.md), or --starter for the example researcher, writer and flow
190
200
  foldrun workspaces demo make the demo pipeline workspace on the platform — "Try the demo pipeline"
191
- foldrun library the account's shared library on the platform: ls [skills|tools|scripts|knowledge|memory], cat <kind>/<path>, put <kind>/<path> (--file, else stdin), rm <kind>/<path> (asks)
201
+ foldrun library the account's shared library on the platform: ls [skills|tools|hooks|scripts|knowledge|memory], cat <kind>/<path>, put <kind>/<path> (--file, else stdin), rm <kind>/<path> (asks)
192
202
  foldrun library new <kind>/<name> a new one from the platform's template, as the Library page's New (--template <transport or gallery entry>, --language)
193
203
  foldrun find <words> search the account as the dashboard's ⌘K does: workspaces, agents, flows, tools, skills, knowledge, memory, runs (--json)
194
204
  foldrun invoke <flow> start a flow on a running platform (--to <workspace>; --once <key> so a retry never starts a second run; --tag <t> repeatable)
@@ -196,8 +206,8 @@ const HELP = `foldrun — agents are just folders
196
206
  foldrun source new <kind> <name> the dashboard's New for any document — tools, knowledge, memory, skills, scripts (agents, flows, evals too): the platform writes the template (--agent <a> inside one agent, --template, --language, --to)
197
207
  foldrun open [page] the dashboard for this workspace, in the browser
198
208
  foldrun api spec the platform's OpenAPI 3.1 document (GET /api/openapi.json) — stdout, or --out <file>
199
- foldrun api version the API version this account's requests get, the versions served, the rate limits — also pin <YYYY-MM-DD>, unpin (admin; --json)
200
- foldrun changelog the platform's release notes, newest first: features, fixes, docs (--limit <n>, --json)
209
+ foldrun api version the API version this account's requests get, the versions served, the rate limits, your last 30 days by version and any deprecated route you still call — also pin <YYYY-MM-DD>, unpin (admin; --json)
210
+ foldrun changelog the platform's release notes, newest first: features, fixes, docs — and the API's versions, breaking changes and deprecations (--limit <n>, --json)
201
211
  foldrun version this CLI's version, its core's, and the platform's: release, API version, component shas — warns when the platform ships a newer CLI (--json)
202
212
 
203
213
  Signing in
@@ -279,7 +289,7 @@ Options
279
289
  --payload <json|text|@f> approve: release a step waiting on wait: event with this body, recorded as you
280
290
  --note "<text>" approve, reject: guidance the agent reads — or the reason for a refusal; billing credit: the ledger line's note
281
291
  --yes approve, stop, rerun (bulk), flow rotate-hook, flow draft, history restore, restore <ws>, import, billing plan, repo, workspace set name, and every rm/revoke/unshare: skip the confirmation, deliberately (required with no terminal)
282
- --json report: the raw run record instead of the report; observe, usage, api version, changelog, find, preferences, workspace vocabulary, flags, workspaces, workspaces show, agent ls, flow ls, agent import --list, backups requests, team: the raw document; version: {cli, core, platform}
292
+ --json report: the raw run record instead of the report; observe, usage, api version, changelog, find, preferences, workspace vocabulary, flags, workspaces, workspaces show, agent ls, flow ls, agent import --list, backups requests, team, hook ls: the raw document; version: {cli, core, platform}
283
293
  --events <a,b> account set notify: failed, awaiting-approval, completed
284
294
  --failed webhooks deliveries: only the ones that gave up
285
295
  --limit <n> runs, billing, history, changelog: how many rows
@@ -290,6 +300,10 @@ Options
290
300
  --account secrets: account scope instead of the workspace's
291
301
  --wait invoke, agent run: hold on and print the result
292
302
  --path <p> tool test: the path an http tool should probe, appended to its base:
303
+ --on <event> hook add: what it listens for — tool.call, tool.result, step.start or step.end
304
+ --tool <pattern> hook add: which tools a tool.call or tool.result hook sees — Bash, Write|Edit, mcp__* (default all)
305
+ --library hook add, hook rm: in the account library (every workspace), not this workspace
306
+ --event <json|@file> hook test: the event the hook reads on stdin (default: a sample for its on:)
293
307
  --watch invoke: follow the run's trace here as it happens
294
308
  --print open: print the URL only
295
309
  --from <n> invoke, flow run, rerun: start at step n; earlier steps are recorded as skipped (a bulk rerun defaults to 1)
@@ -305,7 +319,7 @@ Options
305
319
  --grace <span> keys rotate: keep the old key working for 1h or 24h while you swap the new one in (default 0: revoked now)
306
320
 
307
321
  Platform options (deploy, invoke, secrets, logs, keys)
308
- --to <workspace> workspace on the platform (deploy default: folder name); flow add/show, agent link: edit or show the deployed copy instead of this folder; restore <ws>: the commit, date or age to put it back to (3d, 2026-09-28T14:00)
322
+ --to <workspace> workspace on the platform (deploy default: folder name); flow add/show, agent link: edit or show the deployed copy instead of this folder; hook test: run the deployed hook on the platform; hook ls: list the deployed workspace's hooks and what they did lately; restore <ws>: the commit, date or age to put it back to (3d, 2026-09-28T14:00)
309
323
  --tenant <name> account to deploy into (default: default, local only)
310
324
  --data <dir> the installation's data directory
311
325
  --url <url> a running platform (or FOLDRUN_URL, or where you last signed in)
@@ -338,7 +352,7 @@ if (!command || command === "--help" || command === "-h") {
338
352
  // `--value` as the account's argument and stored an empty secret; `--force
339
353
  // ./dir` swallowed the directory. A flag followed by another flag is also
340
354
  // boolean, so an unlisted switch at least does not eat its neighbour.
341
- const BOOLEAN_FLAGS = new Set(["account", "follow", "force", "oauth2", "wait", "watch", "print", "dry-run", "help", "no-browser", "local", "test", "flat", "yes", "platform", "json", "forever", "all", "check", "new-client", "quiet", "off", "can-ask", "preview", "csv", "failed", "starter", "history", "list"]);
355
+ const BOOLEAN_FLAGS = new Set(["account", "follow", "force", "oauth2", "wait", "watch", "print", "dry-run", "help", "no-browser", "local", "test", "flat", "yes", "platform", "json", "forever", "all", "check", "new-client", "quiet", "off", "can-ask", "preview", "csv", "failed", "starter", "history", "list", "library"]);
342
356
  // Flags said more than once collect into a list: `--tag a --tag b`.
343
357
  const REPEATABLE = new Set(["tag", "expect", "header"]);
344
358
  // `--wait` is a switch everywhere but `flow add <flow> wait`, where it takes
package/docs/agents.md CHANGED
@@ -60,6 +60,7 @@ the point of the week; a step that weighs two arguments and picks one is not.
60
60
  | `agents` | colleagues it may consult mid-run — an answer, no tools |
61
61
  | `subagents` | colleagues it may delegate a whole job to — own context, own tools, never more than its own ([below](#delegating-to-a-sub-agent)) |
62
62
  | `scripts` | programs in `scripts/`, each becoming a callable tool |
63
+ | `hooks` | your own code at every tool call, step start and step end — refuse, rewrite, add context — the same on every model ([Hooks](hooks)). Also in a workspace's or the account's `AGENTS.md` for every agent under it |
63
64
  | `apis` | an HTTP API declared inline, as one tool |
64
65
  | `mcpServers` | an MCP server declared inline |
65
66
  | `secrets` | vault entries its tools may use — names only, never values |
@@ -476,7 +477,7 @@ take, like `eval` or `download`; a misspelt one is refused by `check`).
476
477
  Beside them: `boundaries: true` marks the page's words in every reply,
477
478
  `init:` runs workspace scripts before each page's own, `extensions:` loads
478
479
  unpacked Chrome extensions, `webgpu: true`, `ignore_https_errors: true`, and
479
- `state_key:` names a secret that encrypts saved logins; `video: true`
480
+ `state_key:` names a secret that encrypts saved logins; `state:` names a saved login every call uses (beside `cookies:` it needs `state_key:`, and the secret then seeds it once — see tools.md); `video: true`
480
481
  records every call for the run page to play, and `live_view: false` stops
481
482
  the page being shown live there. See
482
483
  [What this agent's browser may do](tools#what-this-agents-browser-may-do).
package/docs/api-usage.md CHANGED
@@ -50,8 +50,22 @@ The CLI sends the version it was built for on every call.
50
50
  replaces it is released. A pin to a retired version is answered as the
51
51
  current version, never refused.
52
52
  - Every breaking change is announced in **What's new** (the dashboard's
53
- changelog, and `GET /api/changelog`) on the day it ships, with the date the
54
- previous shape stops being served.
53
+ changelog, `GET /api/changelog` and `foldrun changelog`) on the day it
54
+ ships: the version that brought it and what changed, and every deprecated
55
+ route with the date it stops answering.
56
+ - **Checked before it ships.** Every change to the platform is compared with
57
+ the version that is deployed. A route or documented field taken away, a
58
+ type changed, or a request field made required cannot ship unless it comes
59
+ as a new dated version, with the old shape kept for older pins. A route
60
+ can only be removed once its deprecation has passed its sunset date.
61
+ - **You are told if it affects you.** Every request is counted by the
62
+ version that answered it, and a call to a deprecated route by the key or
63
+ person that made it. **Settings → API** and `foldrun api version` show the
64
+ last 30 days. An account that called a deprecated route in the last 30
65
+ days is emailed when it is first seen doing so, and again 90, 30 and 7
66
+ days before the sunset — one email per step, naming the keys that still
67
+ call it. This is required mail: it cannot be switched off, because it is
68
+ the only warning before something you rely on stops.
55
69
  - A route on its way out answers with
56
70
  [`Deprecation`](https://www.rfc-editor.org/rfc/rfc9745) (`@<unix time>` it
57
71
  was deprecated) and, once the date is fixed,
package/docs/api.md CHANGED
@@ -158,6 +158,8 @@ everywhere else a run is live.
158
158
  | `/api/workspaces/<ws>/evals` | GET, POST | list; create (`name`) |
159
159
  | `/api/workspaces/<ws>/evals/<eval>/run` | POST | run one eval |
160
160
  | `/api/workspaces/<ws>/tools/<tool>/test` | POST | exercise one tool (`args`, `path`). An http tool with an `operations:` allowlist is probed only with a GET to one of its operations (no `path`: the first GET without parameters), and fails if the allowlist resolves nothing, as a run does |
161
+ | `/api/workspaces/<ws>/hooks` | GET | the [hooks](hooks) this workspace can turn on — its own and the library's (shadowing marked), invalid ones with the reason, and who turns each on — plus `recent`: every deny, rewrite and failure hooks wrote in the last `?runs=` runs (1–500, default 50), newest first; `?all=1` includes notes and context |
162
+ | `/api/workspaces/<ws>/hooks/<name>/test` | POST | run one [hook](hooks) on a sample event: body `{ event }`, answer the hook's own (`ok`, `decision`, `reason`, `input`, `context`, `reply`, `note`, `failure`, `ms`). Shares the `hooks/` path with flow webhooks below — `/test` is only ever a hook's. Answers `404` for no such hook, `422` for an invalid `hook.md`, `400` for an event that is not an object. On an isolated install hook code never runs in the platform's own process, so this answers with where to test it instead (`foldrun hook test` locally, or a test run) |
161
163
 
162
164
  Running an agent alone is the fastest way to test a prompt change without
163
165
  paying for the steps around it.
@@ -317,7 +319,7 @@ Reads return which secrets exist, never their values.
317
319
  | `/api/account/close` | POST | the owner asks to close the account — in person, never a key, with `confirm` set to the account's name and an optional `reason`. Not a delete: the account is suspended at once (runs in flight finish, sign-in and the export still work), `closeRequested` is set on its admin state, and the super admin is mailed to settle the last invoice and any refund. Returns `{ ok, suspended, notified }`; 409 when already asked. Hosted installs only |
318
320
  | `/api/account/backups` | GET | what is backed up for this account: `policy` (`schedule`, `keepLocal`, `keepOffsiteDays`, `encrypted`, `covers`, `excludes`), `last`, `backups` (each nightly archive that holds this account, newest first: `id`, `at`, `bytes` (the encrypted object), `sha256` (the archive inside the encryption — what a decrypt must reproduce), `kinds` — files, database, keys — `verified`, `offsite`, `keepDays`, `encrypted` (sealed with age before it left the server; false for archives taken before October 2026), `recipient` (the key's fingerprint, `sha256:<16 hex>`, or null)), `requests` (earlier restore requests) and `workspaces` (`name`, `head`, `history`, `lastChange`) — what a restore can reach. See [Backups and restore](backups-and-restore.md) |
319
321
  | `/api/account/backups/requests` | GET, POST | ask the platform team to restore what an account cannot restore itself. POST `{ what, target, workspace?, backupId?, note? }` (`what` is runs, state, storage or everything) records the request, mails the operator and audits it as `backup.restore_requested`; answers 201 `{ ok, request }` with `notified` saying whether the mail went. Restores nothing. `workspace:manage`, and a member scoped to some workspaces must name one. GET lists this account's requests |
320
- | `/api/account/api` | GET, PATCH | how this account meets the API — GET `{ version: { current, pinned, effective, supported, supportMonths }, limits: { plan, windowSeconds, perCredential: { read, write }, perAccount: { read, write } } }`; PATCH `{ version: "YYYY-MM-DD" \| null }` pins the version requests without a `Foldrun-Version` header get (null: always current). `keys:manage`. See [api-usage](api-usage.md) |
322
+ | `/api/account/api` | GET, PATCH | how this account meets the API — GET `{ version: { current, pinned, effective, supported, supportMonths }, limits: { plan, windowSeconds, perCredential: { read, write }, perAccount: { read, write } }, usage }`. `usage` is the last 30 days — `{ days, versions: [{ version, requests, lastSeen }], deprecated: [{ route, requests, lastSeen, deprecatedAt, sunsetAt, link, callers: [{ credential, requests, lastSeen }] }] }`, `credential` naming a key by its label and prefix — or `null` when it could not be read; PATCH `{ version: "YYYY-MM-DD" \| null }` pins the version requests without a `Foldrun-Version` header get (null: always current). `keys:manage`. See [api-usage](api-usage.md) |
321
323
  | `/api/account/flags` | GET | the feature flags as they stand for this account — `{ flags: [{ name, description, owner, default, on, source, rollout }] }`, `source` being what decided it: `account` (an override for this account), `global` (the install-wide setting), `rollout` (a percentage of accounts, by a stable hash of account and flag; `rollout` is the percentage) or `default`. Read-only, every role; only the super admin changes a flag. See [Feature flags](feature-flags.md) |
322
324
  | `/api/openapi.json` | GET | this API as an OpenAPI 3.1 document — every route and method, bearer auth, the rate-limit, version and Idempotency-Key headers, and request/response schemas for runs, flow runs, deploy, source, secrets, keys and billing. No key needed. Generated from the route files by `scripts/gen-openapi.mjs`; the dashboard renders it at Docs → API reference, `foldrun api spec` saves it |
323
325
  | `/api/account` | GET, PATCH | the account's defaults from its AGENTS.md frontmatter — `timezone`, `budget` (a USD cap — a number for a month, `"60/day"`, `"60/week"`, or `{ usd, period }`; null is no limit), `notify` (`{ url?, email?, events }`); PATCH sets the keys sent, null clears one, and edits the file in place |
@@ -374,7 +376,7 @@ What a platform runs, and what changed. See [Versions and releases](versions).
374
376
  | Route | Methods | |
375
377
  |---|---|---|
376
378
  | `/api/version` | GET | open, no key. `{ version, released_at, build, api, components: { core, platform, web, docs, infra, cli }, packages: { core, cli }, browsers, browser_versions }` — `version` is `vYYYY.MM.DD.N` (`dev` on an install the deploy did not build); `components` are short commit shas (`cli` is the CLI commit released alongside); `packages.cli` is that CLI's npm version; `api` is the API contract's date version; `browsers` is the engines the runner image was built with (its `/opt/browser/engines.json`, read by the deploy — see [Engines](tools#engines)), `null` when the deploy did not say; `browser_versions` is their versions where the manifest has a dotted one (`{ chromium: "153.0.8010.12", chrome: "154.0.8037.58", lightpanda: "0.4.1" }`), which `foldrun check` compares with a pinned `user_agent`, `null` when none. `Cache-Control: no-store` |
377
- | `/api/changelog?limit=` | GET | `{ version, releases: [{ version, released_at, previous, components, notes }] }`, newest first — 10 by default, at most 50. `notes` is `{ features, fixes, docs, other }`, each a list of `{ repo, sha, subject }` grouped from commit subjects, or `null` when the deploy could not read what changed. Needs a key or a session |
379
+ | `/api/changelog?limit=` | GET | `{ version, api, releases: [{ version, released_at, previous, components, notes }] }`. `api` is the API's own changelog: `{ current, supportMonths, versions: [{ version, changes }], deprecations: [{ route, deprecatedAt, sunsetAt, link }] }` — every version a caller may pin, newest first, with the breaking changes it introduced, and the routes on their way out. `releases` newest first — 10 by default, at most 50. `notes` is `{ features, fixes, docs, other }`, each a list of `{ repo, sha, subject }` grouped from commit subjects, or `null` when the deploy could not read what changed. Needs a key or a session |
378
380
 
379
381
  Every `/api` response carries the version in its headers, refusals included,
380
382
  so a client learns what it is talking to without a second request:
@@ -6,75 +6,47 @@ different things:
6
6
  | | What it brings back | Who does it | How far back |
7
7
  |---|---|---|---|
8
8
  | **A workspace's history** | its agents, flows, knowledge, skills, tools, scripts — the markdown | you, from Settings → Backups, `foldrun restore` or the API | every change it ever had |
9
- | **The nightly snapshot** | everything else: runs, `state/`, storage's index, secrets, people, keys, the ledger, the audit log | the platform team, when you ask | the snapshots kept (below) |
9
+ | **A database dump before a migration** | the database: people, API keys, the ledger and every balance, the queue, the audit log | the platform team, when you ask | the last 10 deploys that changed the schema |
10
10
 
11
11
  You do not need to switch either on.
12
12
 
13
- ## What the nightly snapshot holds
14
-
15
- On the hosted platform today (a single server), a job runs
16
- **nightly at 03:30, server time (Sydney)** and archives every account
17
- together:
18
-
19
- - the whole data volume: every workspace's files — source, `memory/`,
20
- `state/`, run records — each workspace's git history, the account's
21
- encrypted secrets (the vault) and the storage index;
22
- - the database (`pg_dump`): people, API keys, sessions, the ledger and every
23
- balance, the queue, the audit log;
24
- - the server's key file.
25
-
26
- **Encrypted before it leaves the server.** Every file the job writes — the
27
- archive (with the database dump inside it), the list of what it holds, and
28
- the key file — is encrypted with [age](https://age-encryption.org) to a
29
- public key whose private half is **not** on the server: the server can make
30
- a backup and cannot open one, and neither can anyone holding the object
31
- storage it is copied to. The archive is encrypted as it is written, so it is
32
- never stored unencrypted, even on the server. The job refuses to run rather
33
- than copy anything unencrypted. Snapshots taken before this change
34
- (October 2026) were copied off the server unencrypted, all but the key file:
35
- the first encrypted run deletes those copies from object storage, and the
36
- server's own copies of those nights age out of the 14 kept.
37
-
38
- Before an archive is kept it is checked: the database dump must be readable
39
- by `pg_restore --list` and list table data, row counts are taken from the
40
- live database at the same moment, and the archive is listed in full and
41
- its checksum recorded — of the very bytes being encrypted, in the same pass.
42
- An archive that fails the check is deleted and the older ones are kept.
43
- That an encrypted archive decrypts, with the private key, to exactly that
44
- checksum is proved by an automated test of the whole job on every change to
45
- it; the server cannot prove it nightly, because it does not have the key.
46
-
47
- **Kept:** the last 14 on the server, and 15 days in object storage off it
48
- (Cloudflare R2).
49
-
50
- A full restore from one of these archives was performed on 2026-09-05, from
51
- the R2 copy: the archive matched the server's byte for byte and restored the
52
- workspaces, run records, the database and the sealed key file. That was
53
- before the archive itself was encrypted; *not yet performed* on an encrypted
54
- one — the next restore drill is.
13
+ ## There is no nightly snapshot
14
+
15
+ The hosted platform today is a development server, and since 11 October 2026
16
+ it takes **no nightly backup**. Run records, `state/`, storage's index and
17
+ the vault are on its data volume and are not copied anywhere on a schedule.
18
+ The last full archive is from 7 October 2026 (`foldrun-data-20261007-0330`,
19
+ in Cloudflare R2), encrypted with [age](https://age-encryption.org) to a key
20
+ the server does not hold.
21
+
22
+ What remains is the **database dump taken before every deploy that runs a
23
+ new migration**. It is checked with `pg_restore --list` before it is kept,
24
+ encrypted to the same off-server key (the server can write it and cannot
25
+ read it), and the deploy refuses to go ahead without it, because a migration
26
+ has no way back. The newest 10 are kept on the server and a copy goes to R2.
27
+ It holds the database only, not the data volume.
55
28
 
56
29
  ### What it does not hold
57
30
 
58
- - **Storage file bytes.** On the hosted platform the files in a workspace's
59
- Storage live in object storage (R2), not on the data volume; the snapshot
60
- has the index of what exists, not the bytes. *Unverified:* whether a
61
- storage file deleted by mistake can still be recovered from object
62
- storage. Ask, and say when it was deleted.
31
+ - Workspace files, run records, `state/`, `memory/`, the vault and each
32
+ workspace's git history: the data volume is not backed up. A workspace's
33
+ source can still be restored from its own history (below).
34
+ - **Storage file bytes.** These live in object storage (R2), not on the data
35
+ volume. *Unverified:* whether a storage file deleted by mistake can still
36
+ be recovered from object storage. Ask, and say when it was deleted.
63
37
  - Anything that only existed inside a sandbox while a step ran.
64
38
  - What model providers and connected services hold on their side.
65
39
 
66
40
  A self-hosted install backs up however its operator set it up; this page
67
- describes the hosted platform. `foldrun-infra/dev/backup.sh` is the job, if
68
- you want the same for yours: it needs `age` and `FOLDRUN_BACKUP_RECIPIENT`
69
- (your age public key) and will not run without them.
41
+ describes the hosted platform. `foldrun-infra/dev/ci/premigrate-backup.sh`
42
+ is the pre-migration dump, if you want the same for yours: it needs `age` and
43
+ `FOLDRUN_BACKUP_RECIPIENT` (your age public key) and will not run without them.
70
44
 
71
45
  ## Seeing your snapshots
72
46
 
73
- Settings → **Backups** shows how often, how many are kept, when the last one
74
- was taken and whether it was verified, copied off the server and
75
- **encrypted**, and each snapshot that holds your account, newest first —
76
- each one marked encrypted or not (the ones from before October 2026 are
77
- not).
47
+ Settings → **Backups** and `foldrun backups` list the archives recorded for
48
+ your account. With the nightly job gone, nothing new is added to that list:
49
+ it ends at the last nightly run, on 7 October 2026.
78
50
 
79
51
  ```sh
80
52
  foldrun backups # the same, in the terminal
@@ -84,14 +56,10 @@ foldrun backups # the same, in the terminal
84
56
  GET /api/account/backups
85
57
  ```
86
58
 
87
- Each archive the job verifies is recorded with the accounts it holds; an
88
- account sees only the archives it is in, and its own name as the only entry
89
- in `accounts`. Each record says `encrypted` and
90
- names the key it was encrypted to by fingerprint (`recipient`,
91
- `sha256:` and 16 hex digits of the public key) — a change of fingerprint is
92
- a change of key. *Not yet verified on the server:*
93
- the recording landed on 2026-10-01, so the list starts with the first
94
- nightly run after that deploy — older archives exist but are not listed.
59
+ An account sees only the archives it is in, and its own name as the only
60
+ entry in `accounts`. Each record says `encrypted` and names the key it was
61
+ encrypted to by fingerprint (`recipient`, `sha256:` and 16 hex digits of the
62
+ public key).
95
63
 
96
64
  ## Restoring a workspace yourself
97
65
 
@@ -138,8 +106,9 @@ deploy any one of them — the same operation, by commit instead of by time.
138
106
  ## Asking for a restore from a snapshot
139
107
 
140
108
  Run records, `state/`, storage and the account's records (people, keys,
141
- balances) come back only from a nightly snapshot. That restore replaces a
142
- volume and a database every account shares, and the key that opens the
109
+ balances) come back only from an archive: the database from a pre-migration
110
+ dump, the rest only from the last full archive (7 October 2026). That restore
111
+ replaces a volume and a database every account shares, and the key that opens the
143
112
  sealed half of the archive is deliberately not on the server, so it is done
144
113
  by the platform team rather than by a button.
145
114
 
@@ -170,7 +170,7 @@ avatar, with eight tabs:
170
170
 
171
171
  | tab | what it is |
172
172
  |---|---|
173
- | **Balance** | the number, large, with how long it lasts in words, thirty days of spend as a sparkline, what is held against runs in flight, and Add credit with the amount that covers a month already chosen |
173
+ | **Balance** | the number, large, with how long it lasts in words, thirty days of spend as a sparkline, what is held against runs in flight, and Add credit with the amount that covers a month already chosen. While billing is off on an install (self-hosted, or a test platform), Add credit puts credit on by hand instead — no payment, and an empty balance stops nothing then — the same as `foldrun billing credit <usd>` |
174
174
  | **Plan** | the plan you are on, this cycle's credits and when they reset, and the plans you could move to |
175
175
  | **Usage** | what was consumed, from the run records, and what each flow and agent cost by week from the ledger |
176
176
  | **Statement** | the ledger as a statement: grouped by day, human labels, month-to-date, filters, and a CSV for the month |
package/docs/cli.md CHANGED
@@ -39,7 +39,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
39
39
  | `foldrun agent ls` | the agents deployed in a workspace — name, model, description (`GET …/agents`); `flow ls` the flows and how each fires (`GET …/flows`). `--to <workspace>`, `--json` for the list as the platform returns it |
40
40
  | `foldrun guide` | the coding-agent rules, copied from what Next.js does: a short managed block in the account's `AGENTS.md`, between `<!-- BEGIN:foldrun-agent-rules -->` markers, telling a coding agent (Claude Code, Cursor, Codex) to read `foldrun docs` before it writes an agent, flow or tool, and a `CLAUDE.md` that imports it with `@AGENTS.md`. `foldrun init` writes them, `foldrun pull` refreshes them, and `foldrun check` re-adds them when a coding agent runs it and they are missing or old — as `next dev` does. Only the block is rewritten, in place; text around it is kept. The runtime strips the block, so the agents that run in the account never see it; the rest of `AGENTS.md` stays their shared context. `--check` exits 1 when the block is missing or old (for CI), `--print` writes it to stdout |
41
41
  | `foldrun docs [page]` | these docs, from the copy that ships inside the CLI — the way Next.js ships `node_modules/next/dist/docs` — so a coding agent reads the version it is driving, offline. No page lists them; `foldrun docs flows` prints one; `--path` prints the folder. Design records and the platform operator's routes are left out |
42
- | `foldrun check [dir]` | validate agents, flows, tools, evals, knowledge — offline. At an account root: every workspace, then the shared library. Includes each agent's colleagues: an `agents:` or `subagents:` name that is not an agent, a sub-agent with no `description:`, a sub-agent that would hold none of its tools inside its parent's. Signed in, it also asks the platform's open `/api/version` which Chrome its runner image has, and warns when a pinned `web.browse.user_agent:` claims another major ([why](tools#how-this-agents-browser-presents-itself)); it warns about the old `web.browse.session:` key too (now `vendor_session:`) |
42
+ | `foldrun check [dir]` | validate agents, flows, tools, evals, knowledge — offline. At an account root: every workspace, then the shared library. Includes each agent's colleagues: an `agents:` or `subagents:` name that is not an agent, a sub-agent with no `description:`, a sub-agent that would hold none of its tools inside its parent's. Signed in, it also asks the platform's open `/api/version` which Chrome its runner image has, and warns when a pinned `web.browse.user_agent:` claims another major ([why](tools#how-this-agents-browser-presents-itself)); it warns about the old `web.browse.session:` key too (now `vendor_session:`). Hooks: a `hooks/<name>/` folder that is not a valid hook (`on:`, `run.mjs` beside `hook.md`, `tool:` only on tool events, `timeout:` 1 to 120), and a `hooks:` name in an agent or an AGENTS.md that is in neither `hooks/` nor the library, are errors; a `tool.call` hook on an agent whose `web.search:` (or `web.fetch:`) the model provider runs on its own servers is a warning — those calls never reach the hook |
43
43
  | `foldrun extract [dir]` | move each single-file script tool — a `tools/<name>.md` with its program in a fenced block — into a folder: `tools/<name>/tool.md` with `run:` pointing at `run.js`, `run.py` or `run.sh` beside it. Frontmatter is edited as text, never re-serialised, so the diff is only what moved. A tool that already has a folder is left for you; `--dry-run` lists what it would do |
44
44
  | `foldrun check --to <ws>` | validate the copy that is DEPLOYED, rather than the folder in front of you — a workspace edited through the API, by an agent or by `foldrun source put`, has had no way to be checked short of spending a run. The files are fetched into a temporary folder and the ordinary check runs over them, so there is one copy of the rules; the output names the workspace and the platform it checked |
45
45
  | `foldrun agent run <name>` | run one agent once on a platform, with no flow file: a flow of one step. `--task "…"` is the instruction and is required. Prints the run id and how to follow it; `--wait` holds on (in 25-second pieces, as `invoke` does) and then prints the same report `foldrun report` would; `--test` makes it a [test run](runs#test-runs) |
@@ -61,6 +61,10 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
61
61
  | `foldrun flow paste <flow>` | the canvas's Paste: step markdown from stdin or `--file <path>` goes in as new groups after `--after <g>` (default last), or beside group `--group <g>`, renumbered to fit (core's `pasteSteps`) — `foldrun flow copy-step a --step 2 \| foldrun flow paste b`. Diff, checked, `--dry-run`; `--to` sends `PATCH { edit: { op: "paste" } }` |
62
62
  | `foldrun flow draft "<what it should do>"` | **Draft with AI**, on the platform (`--to <workspace>`; `--flow <name>` redrafts an existing flow): `POST …/flows/draft` has a model write the flow and any agent files it needs, checks and repairs once, and the CLI prints the notes, every file as a diff and what the check still finds. Nothing is saved until you say yes — `--yes` on a pipe, `--dry-run` never; on a pipe without `--yes` it saves nothing and exits 1, though the draft call was made and charged; each file goes through `PUT …/source` with `ifMatch`, so one that changed meanwhile is refused. One model call, charged like a step |
63
63
  | `foldrun tool test <name>` | exercise one tool, alone — no model, no run, no flow. The real thing runs: the HTTP request against the declared `base:`, the script with the workspace's secrets in its environment, the MCP handshake. Prints the transport, whether it exited cleanly, how long it took, everything the tool printed, and any secret it needed by NAME and never by value. Its arguments are `key=value` pairs, one per argument the tool declares, and `--path` is an http tool's probe path. It waits five minutes by default, because a tool that crawls takes as long as it takes — `--timeout <seconds>` waits longer, and a test given up on says so rather than claiming the tool failed |
64
+ | `foldrun hook ls` | the hooks in this workspace (`hooks/<name>/`) and the account library (`library/hooks/<name>/`): each one's `on:`, `tool:` and description, read the way a run reads them — a folder that is not a valid hook is listed with the reason and the command exits 1. A workspace hook shadows a library one of the same name. `--json` for a script. `--to <workspace>` lists the DEPLOYED workspace's hooks instead (`GET /api/workspaces/<ws>/hooks`, the dashboard's Hooks panel): who turns each on, and the denies, rewrites and failures they wrote into recent runs — `--runs <n>` reads further back (default 50, up to 500), `--all` shows every decision |
65
+ | `foldrun hook add <name> --on <event>` | a hook folder: `hook.md` (`on:` one of `tool.call`, `tool.result`, `step.start`, `step.end`; `tool:` from `--tool <pattern>` — `Bash`, `Write\|Edit`, `mcp__*` — on the tool events only) and a self-contained `run.mjs` that allows everything, with the whole contract in its comments: the event on stdin, one JSON answer on stdout, exit 2 to deny with stderr as the reason. `--library` writes it in the account library for every workspace; `--to <workspace>` picks one in an account. An agent turns it on with `hooks: [name]`; `hooks:` in a workspace's or the account's AGENTS.md turns it on for every agent under it |
66
+ | `foldrun hook test <name>` | run one hook HERE on one event, exactly as a step runs it (the event on stdin, a stripped environment, its `timeout:`), and print its answer — decision, reason, a rewritten input, context, reply or note — and how long it took. `--event '<json>'` or `--event @file`; without it, a sample event for the hook's `on:`. A deny is an answer (exit 0); a hook that crashes, times out or answers in the wrong shape is a failure (exit 1) — which, on a gate, refuses the call. `--to <workspace>` runs the DEPLOYED hook on the platform instead (`POST …/hooks/<name>/test { event }`) |
67
+ | `foldrun hook rm <name>` | delete a hook folder (`--library` for the account's). Says which agents and AGENTS.md files still turn it on — their steps are refused until it goes from their `hooks:` — and asks; `--yes` on a pipe |
64
68
  | `foldrun run <target>` | run an agent or a flow HERE, on this machine (`name`, or `flow:name`) — `--test` for a [test run](runs#test-runs). On a platform: `flow run`/`invoke` for a flow, `agent run` for one agent |
65
69
  | `foldrun eval [name]` | one eval, or all of them. `--to <workspace>` runs the DEPLOYED workspace's evals on the platform (`POST …/evals/<name>/run`), waiting for each verdict, and prints the same lines; exits 1 when a case fails |
66
70
  | `foldrun eval new <name>` | the Evals page's New, on the deployed workspace (`POST …/evals { name }`): an eval whose `agent:` is the workspace's first agent and whose case is a placeholder — edit it (`source cat`/`put`), then `foldrun eval <name> --to <workspace>` runs it. `--to <workspace>` |
@@ -95,7 +99,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
95
99
  | `foldrun workspaces show <name>` | what is deployed in one workspace (`GET /api/workspaces/<name>`): its agents with model and description, its flows and how each fires, and its files counted by top-level folder. `--json` for the document |
96
100
  | `foldrun workspaces new <name> --platform` | the dashboard's **Create workspace**: blank — an `AGENTS.md` and nothing to delete (`POST /api/workspaces { name, template: true }`) — or `--starter` for the example researcher, writer and publish flow (`{ name, starter: true }`). The plan's workspace count applies. Without `--platform` it makes the folder here, as `foldrun new` does |
97
101
  | `foldrun workspaces demo` | **Try the demo pipeline**: makes the `demo-pipeline` workspace on the platform — a four-step flow with a parallel pair, an approval gate and a pretend CRM tool (`POST /api/workspaces { demo: true }`) |
98
- | `foldrun library` | the account's shared library on the platform, as the Library page shows it: `ls [skills\|tools\|scripts\|knowledge\|memory]`, `cat <kind>/<path>`, `put <kind>/<path>` (from `--file`, else stdin), `new <kind>/<name>` (the Library page's New, `POST /api/library/<kind> { name, template, language }`: the platform's template — `--template` a transport or a gallery entry, `--language` for a script tool), `rm <kind>/<path>` (asks). `deploy` still pushes the whole of it |
102
+ | `foldrun library` | the account's shared library on the platform, as the Library page shows it: `ls [skills\|tools\|hooks\|scripts\|knowledge\|memory]`, `cat <kind>/<path>`, `put <kind>/<path>` (from `--file`, else stdin), `new <kind>/<name>` (the Library page's New, `POST /api/library/<kind> { name, template, language }`: the platform's template — `--template` a transport or a gallery entry, `--language` for a script tool), `rm <kind>/<path>` (asks). `deploy` still pushes the whole of it |
99
103
  | `foldrun find <words>` | the dashboard's ⌘K search: workspaces, agents, flows, tools, skills, knowledge, memory and runs across the account, ranked the same way (`GET /api/search`; `--json`) |
100
104
  | `foldrun invoke <flow>` | start a flow on a running platform — `--watch` follows its trace here; `--test` makes it a [test run](runs#test-runs) `--once <key>` sends the key as the run's idempotency key, so a CI step that retries after a dropped response is answered with the run it already started instead of paying for a second. `--tag <t>`, repeatable, labels the run (the API's `tags`). `--inputs <name>` sends a saved input set as the task — a case of `evals/<flow>-inputs.md` or of an eval that runs the flow, as the platform has it (see [Run with saved inputs](flows#run-with-saved-inputs)). `foldrun flow run <flow>` is the same command |
101
105
  | `foldrun source <verb>` | one workspace file on a platform: `ls [dir]`, `cat <path>`, `put <path>` (from `--file`, else stdin; `--message` goes on the revision), `new <kind> <name>`, `mv <from> <to>`, `rm <path>` — the editor's own door, so every write is a revision. `new` is the dashboard's New button for any document (`POST …/assets { kind, name, agent?, template?, language? }`): `tools`, `knowledge`, `memory`, `skills`, `scripts` (and `agents`, `flows`, `evals`), the template written by the platform; `--agent <name>` puts a knowledge, memory, skill or script document inside that agent. For a whole tree, `deploy` |
@@ -108,8 +112,8 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
108
112
  | `foldrun onboarding` | the account's getting-started steps — workspace, agent, a run, a run that completes, a secret, a schedule or webhook, a teammate, billing where the plan asks for it, the CLI — each worked out from what is really there, with the next one first and where to do it (`--json`). The dashboard card is hidden per person from the dashboard |
109
113
  | `foldrun preferences` | your own settings on the platform — today the theme (`system`, `light`, `dark`), kept with you on every browser; `preferences set theme dark` changes it (`/api/me/preferences`; a key acts for whoever minted it) |
110
114
  | `foldrun api spec` | the platform's OpenAPI 3.1 document, from `GET /api/openapi.json` (no key needed) — to stdout, or `--out <file>`. Every other command sends `Foldrun-Version` (the API version this CLI was built for), an `Idempotency-Key` on each write (reused on its own retry), and waits out one `429` for `Retry-After` before saying so. See [Using the API](api-usage) |
111
- | `foldrun api version` | Settings → API: the API version this account's requests get when they send no `Foldrun-Version`, whether it is pinned, every version still served and the rate limits. `api version pin <YYYY-MM-DD>` pins it, `unpin` goes back to current (`PATCH /api/account/api`; an admin's) |
112
- | `foldrun changelog` | What's new: the platform's release notes, newest first — features, fixes, docs — and which release this install runs (`GET /api/changelog`; `--limit <n>` up to 50, `--json`) |
115
+ | `foldrun api version` | Settings → API: the API version this account's requests get when they send no `Foldrun-Version`, whether it is pinned, every version still served, the rate limits, and the last 30 days: requests by version, and any deprecated route the account still calls — by which key or person, and when it stops answering. `api version pin <YYYY-MM-DD>` pins it, `unpin` goes back to current (`PATCH /api/account/api`; an admin's) |
116
+ | `foldrun changelog` | What's new: first the API — its current version, the breaking changes each dated version brought, and the routes deprecated with their sunset dates — then the platform's release notes, newest first — features, fixes, docs — and which release this install runs (`GET /api/changelog`; `--limit <n>` up to 50, `--json`) |
113
117
  | `foldrun version` | this CLI's version, its `@foldrun/core`'s, and — when a platform is known (`--url`, `--profile`, `FOLDRUN_URL` or where you signed in) — that platform's: version, release time, API version, each component's commit and the browsers its runner image has (with versions where known: `chrome 154.0.8037.58`), from `GET /api/version` (no key needed). Warns when the platform was released with a newer CLI than this one. An unreachable platform is a dim line, not a failure. `--json` prints `{ cli, core, platform }`. `foldrun --version` (or `-v`) prints only the CLI's version, offline. See [Versions and releases](versions) |
114
118
  | `foldrun doctor` | check the road to the platform, one line each: Node (22 or newer), the CLI and core versions, which of `FOLDRUN_URL`, `FOLDRUN_TOKEN`, `FOLDRUN_TIMEOUT`, `HTTPS_PROXY` are set (never their values), which model credential a local run would use (by its variable, never its value), the account it would act as, DNS for the platform's host, and a timed `GET /api/healthz`. The first thing to run when a command cannot reach the platform |
115
119
  | `foldrun storage <verb>` | what a workspace PRODUCED, as opposed to what you wrote — `source` is the other half. `ls [prefix]` lists every file with its size, when it was written, how long ago and which run wrote it (`run:<id>`, `user:<email>` for an upload, `api-key` for a key), newest first, plus what the store holds against its quota. `cat <path>` prints a text file and refuses one that is not text; `cat <path> --preview` asks the platform to read it instead and prints what is in it — a PDF or Word document as paragraphs, a spreadsheet or CSV as a table, a deck as slides, an archive as its listing. `get <path>` downloads it beside you — `--file <path>` names somewhere else, `--force` overwrites. `put <file|folder>...` uploads any files, of any type, into storage under their own names, walking folders whole (`.git`, `node_modules` and `.DS_Store` are skipped); `--into <folder/>` puts them under a folder, `--as <path>` renames a single file, and large files go straight to the bucket by a presigned URL, so size is not capped by the platform's request limit; `rm <path>` removes one. `share <path>` mints a public link to one produced file (7 days; `--ttl <days>`, `--forever`) and prints the URL; `shares` lists the live links (`--all` includes expired and revoked); `unshare <token>` revokes one — the link answers 404 from then on and the token is never reused. `--to <workspace>` from anywhere; inside a workspace folder it is already known |
@@ -123,7 +127,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
123
127
  | `foldrun billing` | the account's balance, whether billing is on at all, what the platform is currently waiving, and the recent ledger entries with what each was for — a run names its workspace, flow and run id; an adjustment carries its own note. `--limit <n>` shows more. `statement [YYYY-MM]` is a month of the ledger (this month by default) — `--csv` for the file an accountant wants, to stdout or `--file <path>`. `wallet` is the burn, the runway and auto top-up; `wallet set auto-top-up --threshold <usd> --amount <usd>` turns it on (a refill of 5–500), `set auto-top-up off` turns it off, `set email <address>` is where receipts go. `details` is who invoices are made out to; `details set --name "…" --abn … --address "line1, city, state, postcode, AU" --email …` changes it, merged with what is there. `portal` prints a link to Stripe's billing portal. Changing the wallet, the details, and the portal are the account owner's; anyone else is told so |
124
128
  | `foldrun billing plans` | the Plans page: every plan with its monthly fee, credits, runs at once and workspaces, which one the account is on and this cycle's credits left (`GET /api/billing/plans`). `billing plan <id>` chooses one — a first plan answers with Stripe's checkout page, a change on a live plan is prorated at Stripe at once (asks first; `--yes`); `plan cancel` stops it at the end of the cycle, `plan resume` undoes that until then (`POST`/`DELETE /api/billing/subscribe`) |
125
129
  | `foldrun billing top-up <usd>` | the Payment page's top-up: Stripe's checkout page for that amount, opened in the browser (`--no-browser`, or no terminal, prints it). The credit lands when Stripe confirms the payment. `billing card` is the same for saving a card for auto top-up. Both need `billing:manage` |
126
- | `foldrun billing credit <usd>` | put credit on the balance by hand (`POST /api/billing { usd, note }`, `--note "…"` on the ledger line) — only on an install without Stripe, a self-hosted or dev platform; once billing is live the platform refuses it and top-ups go through `billing top-up`. Needs `billing:manage` |
130
+ | `foldrun billing credit <usd>` | put credit on the balance by hand (`POST /api/billing { usd, note }`, `--note "…"` on the ledger line) — only on an install without Stripe, a self-hosted or dev platform; once billing is live the platform refuses it and top-ups go through `billing top-up`. The dashboard's Billing → Add credit does the same while billing is off. Needs `billing:manage` |
127
131
  | `foldrun gallery` | the tools the platform ships to every account (`web`), and for each whether this account keeps its own copy and whether that copy still matches. `pull` lays the shelf down under `~/.foldrun/gallery/<host>/` for offline runs (`foldrun run` and `foldrun eval` refresh it themselves before each run); `upgrade <tool>` replaces the account's copy with the current gallery version, the old one a revision back |
128
132
  | `foldrun account` | the account's own defaults — the AGENTS.md frontmatter every workspace under it inherits: `timezone`, `notify`, `budget`, `concurrency`. `foldrun account set timezone Australia/Sydney`, `set notify email you@example.com` (`--events failed,awaiting-approval,completed`), `set notify url https://…`, `set budget 60/month`, `set concurrency 4`, and `clear <key>` to unset one. `set notify` merges against what is there, because the platform replaces the whole block. `providers` lists the model providers the account's files use and the last daily key check — `--check` asks now, and a dead key is a non-zero exit. `model` is the account's own model key, what every step runs on unless a `provider:` block names another: `foldrun account model` shows it (never the key, only its last four; exit 1 when there is none and runs are refused), `model set <provider> --key <api key>` sets it — a preset name (anthropic, openrouter, openai, groq, …) or `custom --base-url https://… --format anthropic|openai`, `--key -` reads the key from stdin — and `model remove` removes it; a Claude login token is refused for any account not allowed the platform's own credential. `export` downloads everything the platform holds about the account — workspaces, runs, members and the whole ledger — as one JSON file (owner only; `--file <path>`, `--force`). Singular — `accounts` below is the list of logins on this machine |
129
133
  | `foldrun workspace` | one deployed workspace's own settings (`--to <workspace>`): description, timezone, budget and notify, read from its `AGENTS.md` — unset means the account's. `set timezone <tz>`, `set budget 20/week`, `set notify email <address>` / `set notify url <https://…>` (`--events`; merged with what the file says), `set description "…"`, `set name <new>` — a rename changes every webhook URL, so it asks first and names the flows whose URLs changed — and `clear <key>`. Singular — `workspaces` is the list |
@@ -212,6 +216,10 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
212
216
  | `--platform` | `workspaces new`, `agent new`, `flow new`, `tool new`: make it on the platform, not in this folder. `status`: the platform's own health (`--history` for the daily record) |
213
217
  | `--description`, `--model`, `--pattern`, `--template` | `agent new --platform`: its description and model tier. `flow new --platform`: the shape to start from. `tool new --platform`, `source new`, `library new`: a transport or gallery entry to start from |
214
218
  | `--list` | `agent import`: what could be imported, importing nothing |
219
+ | `--on <event>` | `hook add`: what the hook listens for — `tool.call`, `tool.result`, `step.start` or `step.end` |
220
+ | `--tool <pattern>` | `hook add`: which tools a `tool.call` or `tool.result` hook sees — `Bash`, `Write\|Edit`, `mcp__*`; all of them when unset |
221
+ | `--library` | `hook add`, `hook rm`: the account library's `library/hooks/`, not this workspace's |
222
+ | `--event <json\|@file>` | `hook test`: the event the hook reads on stdin (default: a sample for its `on:`) |
215
223
  | `--path <p>` | `tool test`: the path an http tool should probe, appended to its `base:` — a path that escapes the base is refused, the same confinement the agent gets |
216
224
  | `--timeout <s>` | how many seconds one request to the platform may take — or `FOLDRUN_TIMEOUT`. Default 30, and 300 for `tool test`, where the tool itself is the thing being waited on |
217
225
  | `--local` | `deploy`, `secrets`, `logs`: the installation on this machine, even when signed in |
package/docs/hooks.md ADDED
@@ -0,0 +1,139 @@
1
+ # Hooks
2
+
3
+ A hook is your own code at the points every step passes through: before a
4
+ tool runs, after it ran, when a step starts and when it ends. It can refuse
5
+ a call, rewrite it, add context, or rewrite the reply, and it does the same
6
+ thing on every model: Claude, or any other provider through the translator.
7
+
8
+ ```
9
+ workspaces/<name>/hooks/no-force-push/
10
+ hook.md when it runs
11
+ run.mjs what it does
12
+ library/hooks/<name>/ the same, shared by every workspace
13
+ ```
14
+
15
+ ## Turning one on
16
+
17
+ ```yaml
18
+ # agents/deployer/agent.md
19
+ hooks: [no-force-push]
20
+ ```
21
+
22
+ `hooks:` in an agent's frontmatter turns hooks on for that agent. The same
23
+ key in a workspace's `AGENTS.md` turns them on for every agent in it, and in
24
+ the account's `AGENTS.md` for every agent in every workspace. They run in
25
+ that order (account, workspace, agent), each name once. A name is looked up
26
+ in the workspace's `hooks/` first, then the library's. A name that resolves
27
+ to nothing is a `foldrun check` error, and a step that names one is refused
28
+ rather than run without its hooks.
29
+
30
+ ## hook.md
31
+
32
+ ```markdown
33
+ ---
34
+ on: tool.call # tool.call | tool.result | step.start | step.end
35
+ tool: Bash # which tools; Write|Edit, mcp__*, a short name like web
36
+ timeout: 30 # seconds, 1-120 (30 for tool.call/step.start, 60 otherwise)
37
+ description: Refuses force pushes and skipped git hooks.
38
+ ---
39
+
40
+ Why this hook exists, for the next person who reads it.
41
+ ```
42
+
43
+ | `on:` | when | it can |
44
+ |---|---|---|
45
+ | `tool.call` | before a tool runs | allow it, refuse it, or rewrite its input |
46
+ | `tool.result` | after a tool ran | see the input and the result; leave a note on the trail |
47
+ | `step.start` | before the model starts | add context to the task, or refuse the step |
48
+ | `step.end` | after the reply | see it, or replace it |
49
+
50
+ ## run.mjs
51
+
52
+ One self-contained ES module, using Node's built-ins only (no relative
53
+ imports), at most 100 KB. The event arrives as one JSON object on stdin, and
54
+ the answer is the last line of JSON the hook prints:
55
+
56
+ ```js
57
+ let raw = "";
58
+ for await (const chunk of process.stdin) raw += chunk;
59
+ const event = JSON.parse(raw);
60
+ // { event: "tool.call", hook, run_id, step, agent, workspace,
61
+ // tool: { name: "Bash", input: { command: "git push --force" } }, subagent? }
62
+
63
+ const cmd = String(event.tool?.input?.command ?? "");
64
+ if (/\bgit\s+push\b.*\s(--force|-f)(\s|$)/.test(cmd)) {
65
+ console.log(JSON.stringify({ decision: "deny", reason: "force push — use --force-with-lease" }));
66
+ } else {
67
+ console.log("{}");
68
+ }
69
+ ```
70
+
71
+ | event | answer |
72
+ |---|---|
73
+ | `tool.call` | `{}` or `{"decision":"allow"}` lets it through; `{"decision":"deny","reason":"…"}` refuses it (the model reads the reason); `{"input":{…}}` replaces the tool's whole input |
74
+ | `tool.result` | `{}`, or `{"note":"…"}` for the trail |
75
+ | `step.start` | `{"context":"…"}` is added to the task; exit 2 refuses the step |
76
+ | `step.end` | `{"reply":"…"}` replaces the reply |
77
+
78
+ Exit code 2 refuses too, with what the hook printed to stderr as the reason,
79
+ which is the convention most agent tools share.
80
+
81
+ ## When a hook goes wrong
82
+
83
+ A hook that guards (`tool.call`, `step.start`) and throws, times out, exits
84
+ with anything but 0 or 2, or answers with the wrong shape **refuses the
85
+ call**. A guard that crashes must not become an open door. A hook that
86
+ watches (`tool.result`, `step.end`) and fails is recorded, and the step goes
87
+ on, because what it was watching has already happened.
88
+
89
+ When several hooks match one call they run in order. The first refusal ends
90
+ it, and a rewrite is handed to the next hook, so the most restrictive answer
91
+ wins.
92
+
93
+ ## What a hook can reach
94
+
95
+ No more than the agent. A hook runs inside the step's own sandbox, as a child
96
+ of the step, in the agent's directory, with an environment stripped to `PATH`
97
+ and the locale: no secrets, no model key, and a home folder of its own. Its
98
+ code is read when the step starts, so an agent that edits `run.mjs` mid-step
99
+ changes nothing that runs, and a sandbox never writes `hooks/` back.
100
+
101
+ A rewritten tool call goes through the same checks as the original (the
102
+ shell and path confinement, the agent's granted tools, its sub-agents), so a
103
+ hook can narrow what an agent does but never widen it.
104
+
105
+ ## What a hook does not see
106
+
107
+ Tools the model provider runs on its own side never pass foldrun's dispatch.
108
+ With `web: {search: anthropic}` (or another provider's search, or
109
+ `fetch: anthropic`) the search runs at the provider, and a `tool.call` hook
110
+ cannot refuse or rewrite it. foldrun's own `web` tool, the default, goes
111
+ through hooks like every other tool. `foldrun check` warns when an agent with
112
+ a `tool.call` hook uses a provider's own search.
113
+
114
+ ## On the run page
115
+
116
+ Every refusal, rewrite, added context and failure is a line on the step's
117
+ trail (`hook no-force-push denied Bash: force push — use --force-with-lease`)
118
+ and a badge on the run page. The workspace's Hooks page lists its hooks and
119
+ what each one refused or rewrote lately.
120
+
121
+ ## Trying one
122
+
123
+ ```sh
124
+ foldrun hook add no-force-push --on tool.call --tool Bash
125
+ foldrun hook test no-force-push --event '{"tool":{"name":"Bash","input":{"command":"git push -f"}}}'
126
+ foldrun check
127
+ ```
128
+
129
+ `foldrun hook test` runs the hook on your machine exactly as a step runs it.
130
+ `POST /api/workspaces/<ws>/hooks/<name>/test` does the same on the platform
131
+ (see [API](api)).
132
+
133
+ ## Two in the library to copy
134
+
135
+ - **no-force-push** refuses `git push --force` or `-f` (allowing
136
+ `--force-with-lease`), and `--no-verify` or `SKIP_HOOKS=1`.
137
+ - **external-write-log**: when the shell calls `curl` to change something on
138
+ an outside service (POST, PUT, PATCH, DELETE), it writes a line on the
139
+ trail.
@@ -140,6 +140,12 @@ The body, and the webhook's JSON:
140
140
  that wants the data. A webhook URL is the one integration: those services,
141
141
  and every phone-push relay, all accept a POST of that shape.
142
142
 
143
+ Every delivery says which shape its body has, in `x-foldrun-webhook-version`
144
+ (today `1`, also in the `foldrun-webhooks/1` user agent). Within a version the
145
+ body only gains fields, so ignore the ones you do not know. A field removed,
146
+ renamed or given another type is a new version, announced in What's new at
147
+ least six months before the old shape stops being sent.
148
+
143
149
  ## Webhook deliveries, retries and redelivery
144
150
 
145
151
  Every webhook send is a **delivery**: recorded first, attempted at once,
@@ -290,7 +296,7 @@ sending, so it holds for every mail, from every part of the platform.
290
296
  | `security` | password resets, email confirmations, a login moved, an account made for you | no |
291
297
  | `invites` | an invitation to join an account | no |
292
298
  | `billing` | a payment failed, and the grace before runs stop | no |
293
- | `account` | a notification webhook switched off after days of failures; a close request | no |
299
+ | `account` | a notification webhook switched off after days of failures; a close request; an API route the account still calls that is being switched off | no |
294
300
  | `run-alerts` | a workspace's `notify:` email: failed, completed, blocked, quarantined, `sla`, `budget` | yes — per workspace |
295
301
  | `approvals` | a run waiting for you, with approve and reject links | yes — per workspace |
296
302
  | `low-balance` | credits below the warning level | yes |