Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cryochamber

Cryochamber is a hibernation chamber for AI agents (Claude, OpenCode, Codex, Pi, Kimi Code). It hibernates an agent between sessions and wakes it at the right time — not on a fixed schedule. The agent reads its plan, completes a task, and decides for itself when to wake next. That lets AI agents run tasks that span days, weeks, or even years, like interstellar travelers in stasis.

Why not cron?

Cron wakes on a fixed schedule, whether or not there is anything to do. Cryochamber hands the scheduling decision to the agent:

  • It saves tokens. A cron-driven agent burns a full session on every tick, even when nothing has changed. A cryochamber agent sleeps until there is a reason to wake — a TODO it scheduled, or a message in its inbox.
  • It saves your brain. With cron, a human has to guess the right schedule up front: too fast wastes money, too slow misses things. Here the agent reasons about the situation — a deadline that slipped, a review waiting on the author, a chess opponent’s pace — and picks its own next wake.
  • It handles emergencies. When something demands attention, the agent can schedule a wake minutes out, and an inbox message can wake it immediately. Cron cannot speed up when it matters.

Get running in two minutes

Platform support: macOS and Linux only.

cargo install cryochamber
mkdir my-chamber && cd my-chamber
cryo init          # scaffold plan.md and cryo.toml (or let the make-plan skill guide you)
cryo start         # start the daemon, installed as an OS service
cryohub start      # open the printed dashboard URL in your browser

Then edit plan.md to describe the agent’s goal and tasks. Runnable example chambers (mr-lazy, chess-by-mail, and more) live in examples/chambers/ on GitHub.

Watch it work

cryohub start    # prints the local dashboard URL — open it in your browser

Cryohub serves the Agent Console: a main stream with focused thread views for each chamber, plus status, TODOs, notes, log tail, and lifecycle controls on a phone or desktop browser. It is embedded in the cryohub binary — nothing to install. Share a single chamber with someone through an invite link, or bridge it to Zulip with cryo-zulip.

What a chamber guarantees

  • Every wake produces a visible message. If the agent exits without replying, the daemon writes a fallback message — a session is never silent.
  • Every inbox message is answered. Even if the agent crashes mid-session, the sender still gets a reply.
  • Every TODO is honoured. Failed sessions are rescheduled as visible retry attempts with exponential backoff.
  • Nothing is consumed twice. Claimed messages and TODOs never silently become pending again.

Next

  • Getting started — from nothing to a running hub: Pi setup, a first chamber that manages the host, and sharing it with a friend.
  • How it works — a five-minute walkthrough: the chamber files and the session loop.
  • Agent Console — the web and phone UI: sign-in, invites, public deployment.
  • CLI reference — every cryo, cryohub, cryo-agent, and cryo-zulip command.
  • Configuration — every cryo.toml and cryohub.toml field.

Getting started

This page walks one machine from nothing to a running hub, with a first chamber you can read on your phone and share with a friend. Five steps: start the hub, set up the Pi agent, connect, create the first chamber, share it.

Platform: the hub runs on macOS and Linux. The native app runs on Apple Silicon macOS; on a phone, the browser console works as an installable PWA.

1. Start the hub

This guide describes current main, which includes Console and authentication changes newer than v0.2.8. Until the next release, run these commands from a repository checkout with Rust and Node.js 22 installed:

make console-build
cargo install --path . --locked
cryohub start

cryohub start installs the hub as a user service (it survives reboots), binds http://127.0.0.1:8765, and on the first run prints the owner token:

Owner token (save it — or reprint later with `cryohub token owner`):
3f9c…

That token is your login — there are no accounts, passwords, or e-mail. cryohub token owner reprints it any time.

2. Set up the Pi agent

A chamber needs an agent runner installed on the hub host. Pi is the built-in default:

npm install -g @mariozechner/pi-coding-agent
which pi        # the hub verifies this executable exists before starting a chamber

Pi reads provider API keys from the environment, and you do not have to export anything globally: the + New chamber sheet (next step) has a folded API key section that writes the key into that chamber’s own cryo.toml as [provider] env, and the daemon injects it into every session.

Prefer a different runner? claude, opencode, codex, and kimi are in the Settings → Default agent dropdown — see choosing which agent a chamber runs.

3. Connect

On the hub machine itself: open http://127.0.0.1:8765 in a browser and paste the owner token.

From a phone or another machine: the hub stays bound to loopback, so put a TLS-terminating reverse proxy in front of it — the public deployment section is a complete Caddy recipe. Then either:

  • open https://agents.example.com in the phone’s browser and Add to Home Screen, or
  • install the native app and add an owner or invite link. It keeps every saved access and groups the resulting chambers under Owned and Joined; a browser install is bound to the hub that served it.

4. A first chamber: the host manager

A good bootstrap chamber is one that looks after the hub host itself. In the console, tap + New chamber, name it host-manager, paste your API key into the folded API key section, and create it — the sheet scaffolds the chamber and starts it in one action. Then open ⋯ Chamber controls → Plan → Edit plan and give it a brief like:

# Host manager

You look after this machine. Once a day:

- Check free disk space (`df -h /`); warn me when usage crosses 85%.
- Check that `cryohub status` and the other chambers' `cryo status`
  look healthy.
- Report in a few lines; only raise what needs a human.

Wake once a day around 09:00. If I send you a message, handle it and
answer.

Nothing needs a restart after editing the plan: the agent reads plan.md at the top of every session. From here it schedules its own wakes, reports every session into the chamber’s conversation, and answers anything you send it.

5. Share a chamber with a friend

Open the chamber, tap Invite in its header, optionally type the friend’s name, and Copy invite link. Send it to them:

  • opened in a browser, the link is the sign-in — they land directly in that one chamber;
  • pasted into the app’s Add a chamber → Admin or invite link field, it fills the address and token for them.

The link is scoped to that single chamber: a guest can read and send there, and never sees your other chambers or any controls. People with access on the same sheet lists every active link; Remove revokes one instantly. Sharing requires that the friend can reach the hub — that is step 3’s reverse proxy.

How it works

A five-minute walkthrough of a chamber’s moving parts.

A chamber is just a directory

cryo init creates three files:

  • plan.md — the agent’s mission: goal, tasks, and rules. The agent re-reads it at the start of every session.
  • cryo.toml — chamber configuration: which agent command to run, session timeout, inbox watching. See Configuration.
  • NOTES.md — the agent’s memory across sessions. It reads and appends to this file directly.

While the daemon runs, runtime state appears alongside them: logs, todo.json, and messages/inbox/ + messages/outbox/.

The plan is plain markdown

A chamber that watches a GitHub repo for new releases:

# Release watcher

## Goal
Tell me when acme/widgets publishes a new release.

## Tasks
1. Run `gh release list --repo acme/widgets --limit 1` and compare
   the version with the one recorded in NOTES.md.
2. If it changed: send me the release notes with `cryo-agent send`,
   then record the new version in NOTES.md.
3. Schedule the next check with `cryo-agent todo add` — every 2 hours
   on weekdays, once a day on weekends.
4. Hibernate with `cryo-agent hibernate --summary "..."`.

No code, no cron expression — the agent reads the situation (weekday vs. weekend here) and decides the next wake itself.

The session loop

daemon wakes agent        <- earliest TODO due, or inbox message
    │
    v
agent reads plan.md + NOTES.md
    │
    v
does the work
    │
    v
cryo-agent send "..."                    <- a visible message, never silent
    │
    v
cryo-agent todo add "..." --at <when>    <- declares the next wake
    │
    v
cryo-agent hibernate                     <- daemon sleeps until that wake
    │
    └────────────── back to the top ──────────────┘

One wake, one agent run, one return to sleep — that is a session:

  1. The daemon wakes the agent when the earliest pending TODO comes due, or immediately when an inbox message arrives.
  2. The agent reads plan.md and NOTES.md, then does the work.
  3. It sends at least one visible message with cryo-agent send. If it exits without sending, the daemon writes a fallback message — a session is never silent.
  4. It declares its own next wake with cryo-agent todo add "..." --at <time>. The daemon’s next wake is always the earliest pending TODO — no TODO, no wake.
  5. It calls cryo-agent hibernate and exits. The daemon sleeps until the next trigger. hibernate --complete ends the plan for good.

Talking to the agent

Send a message from the terminal (cryo send "...") or the Cryohub dashboard. It lands in messages/inbox/ and, with the default watch_dirs, wakes the agent immediately. The agent’s replies appear in messages/outbox/ and in the dashboard’s message history.

The dashboard opens each thread in a focused view. One cryo-agent receive or cryo-agent dialog call claims one conversation: either the first pending thread or the pending messages in the unthreaded main stream. Once claimed, that conversation must receive a reply before the agent can claim another one. For a thread follow-up, the agent receives the root and earlier replies as context, and its next send returns to the same thread automatically.

Sharing a thread reply to the main stream creates an outbox display copy. It does not add anything to the local inbox, so it neither wakes the agent nor creates work for it.

Next

  • CLI reference — every cryo, cryohub, cryo-agent, and cryo-zulip command.
  • Configuration — every cryo.toml and cryohub.toml field.

Interrupted conversations

Before the daemon archives a claimed conversation, it saves a durable reply obligation. After a hard daemon stop, the next start writes an interruption notice if no reply was saved. The message may have caused partial external work. Review the history and resend only if you still want that work performed. Claimed messages stay archived and are never replayed automatically. An unread batch remains pending. A corrupt recovery journal stops startup with an error instead of discarding it.

See backup and restore for recovery and upgrade procedures.

Agent Console

The Agent Console is the web surface cryohub serves — a phone-first, installable app for reading and steering every chamber the hub knows about. Each chamber has a main stream for reports and instructions. Focused thread views handle follow-ups, and the chamber’s controls are a tap away.

It is embedded in the cryohub binary. There is nothing to install: start the hub and open the URL it prints.

cryohub start        # http://127.0.0.1:8765

Signing in

cryohub start runs in public mode: every /api route needs a bearer token, and the console shows a login screen. The first run creates the owner token and prints it — that line is your login, so keep it:

Owner token (save it — or reprint later with `cryohub token owner`):
3f9c…

Two kinds of token open the console:

  • The owner token. Printed by the first cryohub start, and reprintable any time with cryohub token owner (idempotent — the same secret). Paste it into Access token. This is you: full control of every chamber.
  • An invite link. Someone with the owner token mints a link scoped to one chamber and sends it to you. Opening the link is signing in — the token rides in the #invite= fragment, is stored, and is stripped from the address bar before anything else runs.

There are no accounts, passwords or e-mail. A token is the identity.

cryohub start --no-public opts out: no login, and the hub is open to whoever can reach 127.0.0.1. Sharing and invite links do not work in open mode.

Owner surface vs. guest surface

OwnerInvite holder
Projects listevery chamber, with status dots, next wake, open-question badge, Completed / Archived groupsonly the invited chamber(s), flat
Conversationread, send, upload files, open attachmentssame, for the invited chamber only
⋯ Chamber controls (launch, stop, restart, reset, archive; Todos · Plan · Notes · Settings · Log tabs)yesnever shown
Invite (mint links, People with access, Remove)yesnever shown
+ New chamber, Refresh chambers, Show completed & archivedyesnever shown

The table is a UI decision on top of the real one: the hub classifies every route default-deny. A guest calling an owner route directly — chamber status, todos, lifecycle, sync, token management — gets 403 regardless of what the app draws, and a guest’s live event stream never carries log lines or other chambers’ messages.

Creating a chamber

The owner-only + New chamber sheet creates and starts the chamber in one operation. It uses the host-level default_agent from cryohub.toml; change that command in the Console’s Settings sheet before creating the chamber if needed. The hub verifies that the command’s executable is available before it creates anything. If scaffolding succeeds but the daemon cannot launch, the new chamber remains available and the Console shows the start error so it can be fixed and launched from Chamber controls.

Messages, threads, and sharing

Message bodies support Markdown, including tables and fenced code blocks, plus inline LaTeX between single dollar signs and display LaTeX between double dollar signs. The Console renders and sanitizes this content in the browser.

Reply in a thread when a report needs a focused follow-up. The agent receives the thread root and its reply history with each new follow-up, and its response returns to that thread automatically. Other threads and main-stream messages wait until the current conversation has received a reply.

Click Reply in thread, a reply count, or a new-thread-activity link to open the full-screen thread view. The original message stays at the top, replies appear below it, and the same message box is docked at the bottom. Use Back to stream or press Escape to return. The main-stream draft and uploads remain available while the thread is open. Each thread also retains its own text draft and completed uploads.

Use Share to stream on a thread reply when the result should also appear in the chamber’s main stream. Sharing creates a display copy in the outbox. It does not send a new instruction to the agent or wake it.

Attaching files

Use the paperclip, drop files onto the composer, or paste files from the clipboard. You can stage up to 10 files per message, each no larger than 25 MB. Image previews, upload progress, removal, and retry controls appear before you send. A message may contain only attachments; text is optional.

Sent files appear as download cards. Text files can be previewed inline; PDFs use the browser’s built-in viewer, with a download fallback when unavailable.

Uploaded files that are ready stay attached when a saved draft reloads. Files still queued or uploading live only in the browser tab because the browser does not persist their bytes. If the page reloads before an upload finishes, attach those files again.

Inviting someone to a chamber

  1. Sign in with the owner token, open the chamber, tap Invite in its header.
  2. Optionally name the person (blank becomes guest-1, guest-2, …; names are unique across the hub), then Copy invite link. The link is minted, scoped to that one chamber, and copied in a single gesture.
  3. Send it. It is shown once; the hub does not show it again. Lost link, new link.
  4. People with access on the same sheet lists every active link that reaches this chamber. Remove revokes one after a confirm: the link stops working immediately, the guest’s open event stream ends, and their next request gets 401, dropping them at the login screen with “Your session is no longer valid — please sign in again.” (Opening the revoked link itself says “This invite link is no longer valid.”)

Sharing needs public mode — on an open loopback hub the sheet says so instead of minting a link nobody would need.

The same tokens can be managed from the CLI:

cryohub token create --name alice --chambers qec-decoders   # prints the link fragment once
cryohub token list
cryohub token revoke alice

Choosing which agent a chamber runs

Two dropdowns, both owner-only:

  • Settings → Default agent is the host-wide default, saved to default_agent in cryohub.toml. It is the runner new chambers are created with — by the console’s + New chamber and by a plain cryo init on the same machine. Changing it never rewrites a chamber that already exists.
  • ⋯ Chamber controls → Settings → Agent is one chamber’s own runner, saved to agent in that chamber’s cryo.toml.

Both lists offer pi, opencode, claude, codex and kimi, plus whatever is currently saved — a hand-written command like pi --thinking high, or a path to your own runner, stays selectable rather than being quietly replaced. Anything else you want to run, write into cryo.toml directly.

Saving either dropdown verifies that the command’s executable is available on the Hub host. An unavailable runner is rejected without changing the setting.

The daemon reads cryo.toml when it starts, so changing a running chamber’s agent takes effect on its next restart; the console says so when that is the case. Saving also rewrites cryo.toml, which does not preserve comments in that file.

Editing a chamber’s plan

⋯ Chamber controls → Plan → Edit plan opens plan.md as markdown source and writes it back. Owner-only.

Nothing has to be restarted: the agent is told to read plan.md at the top of every session, so the next wake works from the new brief. Last write wins — the console has no conflict dialog, because the chamber’s own agent is instructed to keep its running state in NOTES.md, which stays read-only here for the same reason.

Installing it on a phone or desktop

The console is a PWA. Once it is open in a browser:

  • Android / Chrome: ⋮ → Add to Home screen (or Install app).
  • iOS / Safari: Share → Add to Home Screen.
  • macOS: Chrome Install, or Safari File → Add to Dock.

The installed app is bound to the hub that served it — one hub per install. The native app lifts that limit, keeps several access links, and groups their chambers under Owned and Joined; see Installing the app. Updates arrive with the hub: after a cargo install cryochamber upgrade and cryohub restart, the open app shows an Update available · Reload bar.

There are no push notifications by design: the app syncs while it is open. It is a console you check, not a pager.

Public deployment (phone outside your network)

cryohub stays bound to loopback. To reach it from a phone on the go, put a TLS-terminating reverse proxy in front of it. Public mode is already on, so there is nothing to switch — just make sure you have the owner token.

cryohub start                # bearer auth on every /api route; prints the owner token on first run
cryohub token owner          # or reprint it later — it is your login

Caddy is the documented proxy. Copy this to /etc/caddy/Caddyfile, replace the hostname (it needs an A/AAAA record pointing at the host before you reload, or no certificate can be issued), and systemctl reload caddy:

agents.example.com {
	encode zstd gzip
	reverse_proxy 127.0.0.1:8765
}

The hub rejects any request whose Host header is neither loopback nor a configured name — that is what stops DNS rebinding — and Caddy forwards the public hostname by default, so allow it in cryohub.toml:

public_hosts = ["agents.example.com"]

(The alternative is header_up Host 127.0.0.1 inside the reverse_proxy block.) Then open https://agents.example.com on the phone, paste the owner token or open an invite link, and Add to Home Screen.

The console’s own pages stay unauthenticated under --public — they are the login screen. Everything under /api is behind the token.

In public mode every credential — guests and the owner alike — is throttled on sends and uploads to a burst of 5 and 10 per minute; past that the hub answers 429 with a Retry-After header. Inbox sends can wake an agent and uploads use the owner’s disk, so the limit keeps an invite link from running up the owner’s bill or filling the chamber with files. Sharing to the stream does not wake the agent.

Serving a build from somewhere else (console_dir)

You never need this to use the console. It exists for development and for running a console build that is newer or different from the one embedded in the binary:

# ~/.config/cryo/cryohub.toml
console_dir = "/home/alice/src/cryochamber/console/dist"

The path must be absolute (the hub canonicalizes it from the service process’s working directory, which launchd/systemd choose). make console-build produces console/dist/; cryohub restart picks it up. cryohub status prints which source is live — Console: embedded or Console: <path> (present|missing).

The hub serves index.html for / and any client-side route, hashed assets from /assets/ with immutable caching, and never lets a request name a file outside the console directory. /api is untouched.

What is stored on the device

The access token, the name the hub knows you by, a per-chamber read watermark, text drafts, ready attachment references, and a small cache of recent messages are stored in localStorage for the hub’s origin. Queued and in-progress file bytes are not stored. Logging out clears the stored data. Message bodies are rendered client-side and sanitized before they reach the DOM.

Installing the app

The Cryochamber app is a native window around the same Agent Console a hub serves in a browser. It exists for one reason a browser cannot cover: it holds several access links at once, groups their chambers under Owned and Joined, and reaches each hub over the OS network stack rather than the page’s own origin.

Nothing about the hub changes. The console a hub serves stays exactly where it was — open http://127.0.0.1:8765 in a browser, or Add to Home Screen it as a PWA, and you get the same surface bound to that one hub. The app is the option for when one is no longer the number of hubs you have.

Release v0.2.8 predates the native app and has no installers. Until a newer GitHub release includes them, follow the source build instructions. The release workflow produces these filenames:

FileFor
cryochamber-vX.Y.Z-android-arm64.apk64-bit ARM Android phones and tablets
cryochamber-vX.Y.Z-macos-arm64.dmgmacOS on Apple Silicon
cryochamber-vX.Y.Z-macos-arm64.app.zipthe same app, zipped, if you would rather not mount a disk image

On macOS take the dmg. Both macOS files carry the identical .app, re-sealed with a full codesign --deep -s - before packaging; the .app.zip exists only for anyone who would rather not mount a disk image.

There is no Play Store, App Store, Windows, or Intel Mac build. What is above is what exists.

Android

  1. Download cryochamber-vX.Y.Z-android-arm64.apk from the release page.
  2. Open it and allow Install unknown apps for the browser or file manager when Android asks. Return to the APK and install it.
  3. Enter a hub address and access token, or paste an admin or invite link into Admin or invite link.
  4. Cryochamber links open in the app. For an ordinary web invite link, use Android’s Share action and choose Cryochamber.

The APK supports arm64 devices running Android 7 or later. It is signed, but it is distributed directly through GitHub rather than Google Play.

macOS

  1. Download cryochamber-vX.Y.Z-macos-arm64.dmg, open it, drag Cryochamber to Applications.
  2. The first launch needs a right-click. The build is ad-hoc signed and not notarized, so double-clicking it gets you “cannot be opened because the developer cannot be verified”. Right-click (or Control-click) the app in Applications → Open → Open. macOS remembers the decision; afterwards it launches normally. On macOS 15 (Sequoia) and later that right-click bypass is gone — let the first launch be refused, then approve the app under System Settings → Privacy & Security → Open Anyway.
  3. Enter a hub address and access token, or paste an admin or invite link into Admin or invite link.

Apple Silicon only. Notarization is not done yet, and this page will say so until it is.

Trust: what the app asks before it sends your token

An access token is a password. Before the app sends one to an address, it decides — visibly — how much that address can be trusted. There are three cases.

HTTPS with a certificate your system already trusts. Nothing is asked. This is a hub behind a reverse proxy with a real certificate (Caddy, as in the console guide) and it is the case to aim for.

Plain http://. A warning appears under the address as soon as you type one, and Add chamber stays disabled until you tick “I understand traffic to this hub is unencrypted”. What it means literally: the token and every message you send travel readable by anything between the device and the hub. On your own machine (http://127.0.0.1:8765) that is nothing at all and the tick is a formality. On a café or conference network it is everyone else on that network. Editing the address unticks the box — the acknowledgement is about one host, not about the form.

HTTPS with a certificate your system does not trust — a self-signed certificate, or a private CA. The app does not silently accept it and does not silently refuse it. It probes the host, then shows an Untrusted certificate sheet with the certificate’s SHA-256 fingerprint as colon-grouped uppercase hex. Compare it against what the hub’s operator reads out:

# on the hub host, against the certificate the proxy serves
openssl x509 -fingerprint -sha256 -noout -in /path/to/cert.pem
# sha256 Fingerprint=88:44:DD:65:…

# or from anywhere, off the live handshake
openssl s_client -connect hub.example:8443 </dev/null 2>/dev/null \
  | openssl x509 -fingerprint -sha256 -noout

The sheet uses exactly that grouping so the two can be read group by group instead of eyeballed as one 64-character run. If they match, Add chamber anyway pins that certificate for that hub from then on. If they do not match, someone else is answering for the hub — Cancel stores nothing.

A pinned hub that later presents a different certificate stops connecting rather than quietly trusting the new one. When the operator legitimately renews, remove the access in Settings → Chamber access and add it again to re-pin.

Several chamber accesses at once

Settings → Chamber access lists every access link the app remembers. Each row shows its label, hub address, Owner or Guest role, and cryohub version, with Add chamber at the bottom and Remove on each row. Adding a second token for the same hub keeps both scopes; it never replaces the chambers already saved.

The main list has Owned and Joined sections. If an owner token and an invite token both expose the same chamber on the same hub, the app shows it once under Owned so the admin controls remain available. Unread counts, drafts, and read watermarks stay separate for every saved access.

An owner can use Settings → Chambers → Copy admin link. The app warns first because anyone holding that link can administer every chamber on the selected hub. The copied cryochamber:// link opens the add-chamber form directly.

Hubs fail independently. A hub that stops answering has its rows’ chips read · unreachable and go muted — the row still shows the last thing that hub said rather than disappearing — while every other hub keeps streaming, sending, and updating. When it comes back, its rows recover on their own; no restart, no re-entering the token.

Hub accounts live in the app’s own private data directory rather than in browser storage, so clearing a browser or losing a WebView’s data does not cost you your tokens. On macOS that file is ~/Library/Application Support/com.cryochamber.console/hubs.json. It holds bearer tokens in the clear, protected by the file permissions on that directory — treat it the way you treat ~/.config/cryo/cryohub-tokens.json.

Updating

There is no in-app updater and no update channel. Download the next release’s build and install it over the old one.

  • macOS: replace Cryochamber in Applications. The hub store lives outside the bundle and is untouched. The right-click gesture may be needed again for the newly downloaded copy.
  • Android: install the newer APK over the old one. The signing key stays the same, so Android treats it as an update and preserves the app’s hub store.

The hubs themselves upgrade separately (cargo install cryochamber, then cryohub restart). If a hub is older than the app, Settings → Chamber access says so on that hub’s row — “hub is older — some features may be missing”.

CLI reference

All cryochamber binaries and their commands. For cryo.toml and cryohub.toml fields, see Configuration.

Every binary accepts --version (print the version and exit) and --help.

Operator CLI (cryo)

Run these from inside a chamber directory unless noted otherwise.

CategoryCommandWhat it does
Lifecyclecryo init [--agent <cmd>]Initialize the directory: write cryo.toml, plan.md, NOTES.md, and README.md. Existing files are kept. Without --agent, uses the host-level default_agent from cryohub.toml (built-in default: pi).
cryo start [--agent <cmd>]Start the daemon. Reads cryo.toml and writes overrides to timer.json.
cryo start --max-session-duration 3600Override the session timeout for this run.
cryo statusShow whether the daemon is running, the current session number, and the next wake time.
cryo restartRestart the running daemon. When it is installed as an OS service, restart the existing service without rewriting or removing it.
cryo cancelStop the daemon and remove the runtime state.
Logscryo watch [--all] [--viewpoint cryo|agent]Follow a log in real time. --all shows the log from the beginning. --viewpoint cryo (default) follows the structured event log; --viewpoint agent follows raw agent output (cryo-agent.log).
cryo logPrint the full session log.
Messagingcryo send "<message>" [--from <name>] [--subject <text>]Send a message to the agent's inbox; the daemon's inbox watcher wakes the agent. --from sets the sender (default human), --subject sets the subject (default: derived from the body).
cryo receiveRead messages the agent sent to the outbox.
Housekeepingcryo clean [--force]Remove runtime files such as logs, state, and messages.
cryo ps [--kill-all]List, or kill, every running cryo daemon on this machine. Run from anywhere.

Hub (cryohub)

CommandWhat it does
cryohub start [--host <ip>] [--port <n>] [--default-agent <cmd>]Install a service that survives reboot. Enforces bearer auth by default, printing the owner token on first run (cryohub token owner reprints it). The supplied host, port, and default agent update the saved host config.
cryohub start --foregroundRun the hub in the current terminal instead of installing a service.
cryohub stopUninstall the global hub service.
cryohub restartRestart the installed global hub service without reinstalling it.
cryohub statusShow the global hub URL, mode (public (bearer auth) or open (loopback)), chamber root, config path, log path, console source (embedded, or the console_dir override and whether a build is present), and service status. Also lists legacy cwd-scoped hub services from older versions.
cryohub start --publicEnforce bearer-token auth on every /api route — the default. Creates the owner token if there is none and prints it. Saved to cryohub.toml, so a later plain cryohub start, a restart, or a reboot stays authenticated.
cryohub start --no-publicRun without authentication (open mode, loopback only). Sharing and invites do not work in open mode. Required: disabling auth is never implicit, and a later plain cryohub start keeps the saved open mode.
cryohub token ownerPrint the owner token, creating it on first use. Idempotent — repeat runs print the same secret.
cryohub token create --name <name> --chambers <id,...>Mint a named invite scoped to those chamber ids. Prints the token and its #invite= link fragment; this is the only time the secret is shown.
cryohub token listList invites with scope, creation time, and revocation status. Never prints token strings.
cryohub token revoke <name>Revoke an invite by name. Takes effect immediately, including on already-open SSE streams. Fails if no active invite has that name.

Agent IPC (cryo-agent)

These commands are used by the spawned AI agent to communicate with the daemon over a Unix socket. They are not the operator interface.

CategoryCommandWhat it does
Hibernatingcryo-agent hibernate --summary "..."End the session; more work remains. Refused (non-zero exit) while unread inbox mail exists — the agent must receive, reply, and retry, so a session never ends with mail waiting for it. Also refused while no pending TODO declares the next wake. A successful call may block up to the reply window the agent requested with --linger <seconds> (omitted = 300, capped at 86400; 0 sleeps immediately).
cryo-agent hibernate --completeEnd the session; the plan is done. Additionally refused while a TODO is due. Never held open by the reply window.
cryo-agent hibernate --exit 1Report a failed session. The daemon marks consumed TODOs done and adds a fresh numbered retry TODO. Failure reports are never refused and never held open.
TODOscryo-agent todo add "text" --at <TIME>Schedule the next wake via a TODO. --at accepts a relative offset (+30 minutes), an ISO 8601 timestamp (2026-04-25T10:00; seconds and a space separator are tolerated), or a date only (2026-04-25, meaning midnight).
cryo-agent todo listList all TODO items.
cryo-agent todo done <id>Mark a TODO item as done.
cryo-agent todo remove <id>Remove a TODO item.
Messagingcryo-agent send "message"Write a message to the outbox for the human. After claiming a thread, the daemon routes the message back to that thread automatically.
cryo-agent send --stdinRead the outbox message body from stdin exactly, including trailing newlines; use for multi-line or shell-sensitive text.
cryo-agent send --question "msg"Mark the message as a question awaiting a human reply.
cryo-agent receiveClaim one pending conversation from the human: the first pending thread, or the unthreaded main-stream messages. A thread claim includes its root and reply history. Hub attachment links are returned as local messages/attachments/... paths. Reply before claiming another conversation.
cryo-agent dialog [--last N | --all | --since <iso>]Render the conversation transcript (default: last 20 messages). --last N shows the last N, --all shows every archived message, --since <iso> shows messages at or after an ISO 8601 time; the three are mutually exclusive. Also claims and archives at most one pending conversation. A new thread claim includes its root and full history despite the requested limit; later reads stay in the active thread and honor that limit. Hub attachment links become local messages/attachments/... paths.
Timecryo-agent timePrint the current local time in ISO 8601 format.
cryo-agent time "+30 minutes"Compute a relative offset. Units: minutes, hours, days, weeks.
cryo-agent time "2026-04-25T10:00"Validate and normalize an ISO 8601 timestamp.

Zulip Sync (cryo-zulip)

CommandWhat it does
cryo-zulip init --config <zuliprc> --stream <name> [--topic <topic>] [--history]Validate credentials, resolve the stream, and write zulip-sync.json.
cryo-zulip sync [--interval N]Start the background sync daemon. Default interval comes from cryo.toml or falls back to 5 seconds.
cryo-zulip unsyncStop the sync daemon.
cryo-zulip pullOne-shot pull.
cryo-zulip pushOne-shot push.
cryo-zulip statusShow sync configuration.

Configuration

Each chamber is configured through a cryo.toml file in its directory. cryo init creates one with sensible defaults.

cryo.toml

# cryo.toml — cryochamber project configuration
agent = "pi"                     # Agent command (pi, opencode, claude, codex, kimi, ...)
max_session_duration = 3600      # Session timeout in seconds (0 = no timeout)
watch_dirs = ["messages/inbox"]  # Directories to watch for reactive wake ([] disables)
zulip_poll_interval = 5          # Zulip sync poll interval in seconds

# Provider environment injected into every agent session (optional).
[provider]
name = "anthropic"               # Display name, shown in `cryo status`
env = { ANTHROPIC_API_KEY = "sk-ant-..." }  # Env vars set when spawning the agent
FieldDefaultDescription
agent"pi"Agent command to run. Use "opencode" for OpenCode, "claude" for Claude Code, "codex" for Codex, "kimi" for Kimi Code, or any executable on PATH. New chambers use the host-level default_agent unless cryo init --agent supplies an explicit command. An owner can also change it from the Agent Console — chamber controls → Settings → Agent — which rewrites this file (comments and any keys Cryochamber does not recognise are not preserved) and takes effect on the chamber’s next restart.
max_session_duration3600Session timeout in seconds. 0 disables the timeout.
watch_dirs["messages/inbox"]List of directories the daemon watches for new files to wake the agent reactively. Paths are interpreted relative to the chamber directory unless absolute. Set to [] to disable reactive wake entirely.
zulip_poll_interval5How often cryo-zulip sync polls Zulip, in seconds. cryo-zulip sync --interval N overrides it for one run.

The reply window is not configured here. How long a successful hibernate stays open for a follow-up message is chosen by the agent per hibernate via cryo-agent hibernate --linger <seconds> (omitted = 300 s, capped at 86400; 0 sleeps immediately). The session clock is suspended while a hibernate is parked and each follow-up round gets a fresh budget, so a generous linger can hold one session open well past max_session_duration.

[provider]

Cryochamber supports a single active provider profile. The [provider] table carries a display name and an env map of environment variables that are injected into every spawned agent session — this is where API keys for the agent’s model belong.

[provider]
name = "anthropic"
env = { ANTHROPIC_API_KEY = "sk-ant-...", OPENCODE_MODEL = "claude-sonnet-4-20250514" }

cryo status shows the provider name once one is configured.

Security: values under [provider].env are secrets. cryo init writes a chamber .gitignore that ignores .cryo/, but cryo.toml itself is not gitignored — if you commit or push the chamber, keep API keys out of version control. Either add cryo.toml to your own .gitignore, or leave the keys out of cryo.toml and export them in the environment before cryo start instead.

Legacy [[providers]] (deprecated)

Older configs used a [[providers]] array. It is still accepted for backward compatibility, but only the first entry is used — provider rotation was removed. Loading a config that uses [[providers]] prints a deprecation warning, and the next save rewrites it to the canonical single [provider] form. Migrate to [provider].

See cryohub.toml below.

cryohub.toml

Cryohub settings live in $XDG_CONFIG_HOME/cryo/cryohub.toml, or ~/.config/cryo/cryohub.toml if XDG_CONFIG_HOME is unset. The default local dashboard URL is http://127.0.0.1:8765. The dashboard’s New Chamber button creates chambers under the configured chamber_root, which defaults to ~/.cryo/chambers.

host = "127.0.0.1"
port = 8765
chamber_root = "/Users/alice/.cryo/chambers"
default_agent = "pi"
public = false
owner_name = "human"
public_hosts = []
# console_dir = "/absolute/path/to/console/dist"   # optional override, see below

For project-owned chamber collections, set chamber_root to a project path such as /path/to/project/.cryo/chambers.

Unknown keys are rejected: a typo such as console-dir fails cryohub start with an error naming the key rather than being silently ignored.

FieldDefaultDescription
host"127.0.0.1"Bind address for the global dashboard service.
port8765TCP port for the global dashboard service.
chamber_root~/.cryo/chambersDefault location for chambers created from the dashboard UI.
default_agent"pi"Host-level agent command for new chambers created by either the Console or plain cryo init. Change it in the Console’s Settings sheet, edit this file, or run cryohub start --default-agent <cmd>. An explicit cryo init --agent <cmd> overrides it. Existing chambers keep their own cryo.toml.
publictrueWhether bearer-token auth is enforced on every /api route. On by default; a config file written before this default that omits the key also loads as true, while an explicit public = false stays open. Cleared only by cryohub start --no-public — a plain cryohub start keeps whatever is saved here.
owner_name"human"Sender name stamped on messages the owner sends in public mode. A client-supplied from is ignored.
public_hosts[]Extra Host header values to accept, on top of loopback and host. Needed when a reverse proxy forwards the public hostname.
console_dir(unset — embedded)Serve the Agent Console from this directory instead of the build embedded in the cryohub binary. Must be an absolute path to a vite dist/. Development and custom builds only.

The Console updates default_agent without a hub restart. The next chamber it creates uses the new command; no existing chamber is rewritten. A hand edit to this file, like every other key here, is picked up on the next cryohub restart. Saving the setting, changing a chamber’s agent, passing cryohub start --default-agent, and starting a chamber all verify that the command’s executable is available. The Console’s New Chamber action creates and starts the chamber in one operation.

Serving the Agent Console

The Agent Console is the hub’s web surface — there is no other dashboard — and it is embedded in the binary: cryohub start serves it with no configuration.

The hub answers / and any client-side route with the console’s index.html, serves hashed assets from /assets/ with immutable caching, and keeps /api untouched. Nothing outside the console source is reachable — a ../ path or a symlink pointing out of an override directory is a 404. The console’s own pages stay unauthenticated even under --public, because they are the login screen; every /api route stays behind the bearer token.

Set console_dir only to serve a different build (make console-build writes console/dist/). Make it absolute: the hub canonicalizes it from the service process’s working directory, which launchd/systemd choose. cryohub status reports which source is live. A hub whose override directory has no index.html — or a binary built without the console and no override — answers pages with a short setup page (HTTP 503) rather than a bare 404; the API keeps working throughout.

Behind a reverse proxy

The hub rejects any request whose Host header is neither loopback nor a configured host — that is what stops a malicious page from scripting the loopback service via DNS rebinding. A proxy that preserves the public hostname (Caddy’s default) therefore needs that name allowed:

public_hosts = ["agents.example.com"]

The alternative is to make the proxy rewrite it — in Caddy, header_up Host 127.0.0.1 inside the reverse_proxy block.

Override config from the command line

Flags passed to cryo start override cryo.toml for that session. The overrides are stored in timer.json (runtime state) and do not modify cryo.toml.

cryo start --agent claude
cryo start --max-session-duration 3600

Config vs. state

FilePurposePersists across runs
cryo.tomlProject configuration. Check into git.Yes
timer.jsonRuntime state: session number, PID lock, CLI overrides.No

Operating and releasing Cryochamber

Backup and restore

Back up a stopped system so mailbox moves and config updates cannot race the copy. Schedule a maintenance window, stop incoming chat bridges and stop the hub with cryohub stop. In each chamber run cryo cancel; if native Zulip sync is enabled, run cryo-zulip unsync. For the Python bridge, run chat-bridge unsync --chamber <path>. Check that no chamber or bridge process remains.

Copy the entire chamber directory, including plan.md, NOTES.md, cryo.toml, todo.json, timer.json, logs, messages, attachments and .cryo. Include the reply-obligation journal if present. Exclude .cryo/cryo.sock, which is a runtime socket. Preserve permissions and symlinks. For example, from the chamber’s parent:

umask 077
tar --exclude='./my-chamber/.cryo/cryo.sock' -czf chamber-backup.tar.gz ./my-chamber

Also copy host configuration from ${XDG_CONFIG_HOME:-$HOME/.config}/cryo and the chamber registry from $XDG_STATE_HOME/cryo/chambers if XDG_STATE_HOME is set, otherwise ~/.cryo/chambers. Backups contain API keys and bearer tokens; store them with restricted access and encryption provided by your backup system. Native device keys are not portable backups. Issue new hub tokens on a new device.

Restore into a separate directory first, with all services still stopped. Check the restored config, plan, TODOs and mailbox history. Remove a restored socket and clear only the process identity in timer.json:

python3 - <<'PY'
import json
from pathlib import Path
path = Path('timer.json')
state = json.loads(path.read_text())
state['pid'] = None
state['instance_id'] = None
path.write_text(json.dumps(state, indent=2))
Path('.cryo/cryo.sock').unlink(missing_ok=True)
PY

Preserve session activity, TODO claim state and the reply journal. They tell the daemon what was interrupted. Start the restored chamber only after disabling the original instance and checking its runner credentials. Confirm that history is present, interrupted claims produce a notice, and a newly sent instruction gets a reply. Then restart the hub and bridges. Never run two copies of the same restored chamber against the same external service.

Before an upgrade, record the installed version and keep its binary plus a consistent backup. Roll back the binary and its matching data snapshot together; do not point an older binary at an unreviewed newer data format.

History and performance

The Console requests the latest 100 messages and loads earlier pages on demand. The page cursor uses immutable mailbox filenames, which Cryochamber prefixes with the send timestamp. Moving a file to archive preserves its cursor. Manually named legacy files retain lexical page order; timestamps still order messages within the displayed window. Legacy API clients can still request the full array by omitting limit. A page accepts limit=1..100 and an optional before cursor.

Paging lists filenames but only opens message bodies in the selected window. Session logs are still read in full. The initial qualification envelope is 20 chambers and 10,000 messages per chamber; this is a test target, not a latency guarantee. Measure optimized builds on named hardware before publishing an SLO.

Release checks

Version tags run the normal Rust, Console, Python and native-shell checks plus dependency auditing. The workflow creates a draft release, builds native assets, publishes the crate only after successful builds, then publishes the draft. An unsuccessful run must leave the release draft for investigation. Crate publication and GitHub release publication are separate operations, so verify both before retrying a partially completed release.

Before tagging, run the packaged-app smoke checklist in app/README.md on macOS and Android. Record device/OS versions, commit, installer checksums and results. Include first install, upgrade, cold restart, credential migration, offline recovery, certificate changes, native Back, keyboard focus and token revocation. A CI build is not a substitute for these checks.

Customer macOS releases require these GitHub secrets:

  • APPLE_CERTIFICATE, the base64 Developer ID Application certificate in p12 format.
  • APPLE_CERTIFICATE_PASSWORD and APPLE_SIGNING_IDENTITY.
  • APPLE_API_ISSUER, APPLE_API_KEY, and APPLE_API_KEY_CONTENT, the App Store Connect private key contents.

The pipeline verifies the signature, notarization ticket and Gatekeeper assessment. Local ad-hoc builds remain available for development. Follow the Tauri signing guide when provisioning credentials. Android uses the existing release keystore secrets.

Require PRs and passing component/security checks on main, prohibit force pushes and deletion, and require the branch to be current before merge. A solo maintainer can use zero required human approvals; passing checks remain required.

Dependency findings

Run the pinned cargo-audit scanner against both Cargo lockfiles and npm audit against the Console lockfile. Network failure means the check is incomplete. Scheduled checks catch new advisories even when dependencies have not changed.

The 2026-09-05 native audit identified unmaintained GTK3 bindings and Unicode dependencies, and RUSTSEC-2024-0429 in the Linux-only GLib 0.18 dependency. macOS and Android are the supported native release targets; a Linux native release requires resolving that GLib finding first. Track these upstream dependencies rather than changing incompatible transitive major versions by hand. Owner: repository maintainer. Reassess by 2026-12-01, or when Tauri changes its Linux backend. The audit gate denies soundness and yanked-package findings. It exempts only RUSTSEC-2024-0429 until that date; CI fails when the exception expires. Maintenance warnings remain visible in audit output. The yanked chacha20 0.10.1 dependency was updated to 0.10.2 with unchanged dependency requirements.