Tidebreak documentation
Running headless
The tidebreak CLI — serve, one-shot prompts, and the MCP stdio server.
The desktop app embeds a local HTTP + WebSocket server. The same server, agent loop, and tools run from a command line without the window.
Install the command
The tidebreak command ships inside the desktop app. To run it from a
terminal, install it from the app:
-
In the Tidebreak menu, choose Install the tidebreak Command. You can also open Settings → Command line and choose Install the tidebreak command.
-
Tidebreak links
tidebreakinto~/.local/bin, creates that folder if it does not exist, and says whether the folder is on your PATH. -
If the folder is not on your PATH, add this line to your shell profile, such as
~/.zshrc, and open a new terminal:export PATH="$HOME/.local/bin:$PATH"
To check the install, run:
tidebreak --versionTo install the command for every account on the Mac, open Settings →
Command line and choose Install for all users. macOS asks for an
administrator password, and Tidebreak links /usr/local/bin/tidebreak.
The link points into the app bundle, so an app update keeps the command
current. If you move the app, install the command again to repair the link.
Tidebreak refuses to replace a tidebreak it did not install, and Uninstall
the command in the same settings page removes only a link Tidebreak made.
The examples below use the installed tidebreak command. To run the CLI from
a clone instead, see Build from source.
Which data does the CLI use?
A command works on one profile: a data directory with its chats, settings, and logs, plus the credentials stored for it.
TIDEBREAK_DATA_DIRunset. The CLI uses the Tidebreak app's own profile. A command connects to the app while the app is running. When the app is not running, the command stops and tells you so. Add--embedto work on the app's data without opening the app: the command then runs the server in its own process.TIDEBREAK_DATA_DIRset. The CLI uses the profile in that folder, and a command runs its own server over it. Add--attachto connect to a server that already owns the folder. This profile keeps its credentials apart from the app's, so a key you set or remove here never changes the app.--server <url>. The command connects to that server and uses no local data.
Nothing uses the folder you run a command from. serve, folder, and
rehome-secrets follow the same rule: the app's data unless
TIDEBREAK_DATA_DIR names another folder. Because one data directory has one
server at a time, serve over the app's data cannot run while the app does.
A command trusts the app's listen.json only while the app holds its data
directory, and only after the server the file names answers as Tidebreak.
That first request carries no credential, so a port the app left behind never
receives one.
Upgrading from an earlier version
Before this change, every CLI profile shared the app's keychain entry. A
profile you name with TIDEBREAK_DATA_DIR now has an entry of its own, and
after the upgrade that entry is empty. To bring back the keys the profile
stored before, run this once:
TIDEBREAK_DATA_DIR=/path/to/profile tidebreak rehome-secretsIt copies the shared entry into the profile's own and leaves the app's entry
as it is. The copy holds the app's keys too, because the two shared one entry;
remove any the profile should not use with provider remove-key. macOS may
ask once to let the CLI read the app's entry. Nothing copies the entry unless
you run this: a new profile starts empty.
A .tidebreak folder an earlier CLI created in a project is such a profile.
To keep using it, set TIDEBREAK_DATA_DIR to that folder and run the command
above.
A newer CLI upgrades the app's data
serve and --embed open the app's data with the CLI's own version. A
release build from source opens the installed app's data, and a debug build
opens the dev app's. If that CLI is newer than the app, the first such command
upgrades the app's database, and the older app then refuses to open it.
Tidebreak saves a copy in the data directory's backups/ folder before the
upgrade. To go back, install the newer app, or restore that copy (see
Troubleshooting).
Connecting to the running app upgrades nothing.
Commands
tidebreak serve
tidebreak mcp <workspace>
tidebreak rehome-secrets
tidebreak -p <prompt> [--chat <id>] [--output-format text|json]
[--permission-mode ask|auto|allow|plan]
[--model <key>]
tidebreak output list <chat>
tidebreak output show <chat> <output> [--revision <id>]
tidebreak output revisions <chat> <output>
tidebreak output export <chat> <output> <path> [--revision <id>]
tidebreak attach <chat> <file>
tidebreak provider list
tidebreak provider set-key <kind> [--from-env <var>]
tidebreak provider remove-key <kind>
tidebreak model list
tidebreak model roles
tidebreak model select <key|auto> [--role <role>]
tidebreak settings show
tidebreak settings web-search select <provider|off>
tidebreak settings web-search set-key <provider> [--from-env <var>]
tidebreak settings web-search remove-key <provider>
tidebreak settings exec select <provider|off>
tidebreak settings exec set-key <provider> [--from-env <var>]
tidebreak settings exec remove-key <provider>
tidebreak mcp-server list
tidebreak mcp-server add <name> (--command <cmd> [--arg <a>]… | --url <url>)
tidebreak mcp-server remove <name>
tidebreak plugins install --git <url> --ref <tag-or-sha> [--json]
tidebreak chat list [--archived]
tidebreak chat create
tidebreak chat delete <chat>
tidebreak chat pin|unpin <chat>
tidebreak chat archive|unarchive <chat>
tidebreak chat steer <chat> <turn> <text...>
tidebreak chat retry <chat> [--turn <turn>] [--wait]
tidebreak chat regenerate <chat> [--turn <turn>] [--model <key>] [--wait]
tidebreak chat edit <chat> <text...> [--turn <turn>] [--wait]
tidebreak chat branch <chat> [--turn <turn>]
tidebreak agent-run list <chat>
tidebreak agent-run show <chat> <run>
tidebreak agent-run cancel <chat> <run>
tidebreak diagnostics snapshot
tidebreak diagnostics metrics
tidebreak diagnostics export <path>
tidebreak data show
tidebreak data backup <path> [--force]
tidebreak data export <path> [--format markdown|json] [--chat <id>]… [--force]
tidebreak folder connect <path> --chat <id>
tidebreak folder list [--chat <id>]
tidebreak folder disconnect <path-or-root-id> --chat <id>
tidebreak agent-mcp
tidebreak code doctor [--refresh]
tidebreak code repo add <path> [--name <name>] [--base-ref <ref>] [--branch-prefix <p>]
tidebreak code repo list
tidebreak code repo rm <id>
tidebreak code ws new --repo <id|path> [--title <title>] [--base-ref <ref>]
tidebreak code ws list [--repo <id|path>]
tidebreak code ws show <id>
tidebreak code ws archive <id> [--force]
tidebreak code session start --ws <id> --harness <kind> [--mode plan|ask|auto|allow]
[--model <id>] [--reasoning <level>] [--fast]
tidebreak code session show <id>
tidebreak code session mode <id> plan|ask|auto|allow
tidebreak code session reap <id>
tidebreak code share grant <session-id> <subject> [--level view|contribute]
tidebreak code share list <session-id>
tidebreak code share revoke <session-id> <subject>
tidebreak code share visibility <session-id> private|deployment
tidebreak code run (--session <id> | --ws <id>) [<message>] [--on-approval wait|fail] [--timeout <secs>]
tidebreak code approvals [--session <id>]
tidebreak code approve <approval-id>
tidebreak code deny <approval-id> [-m <feedback>]
tidebreak code interrupt --session <id>
tidebreak code turns --session <id>
tidebreak code diff --ws <id> [--turn N] [--file PATH]
tidebreak code files --ws <id> [--turn N]
tidebreak code restore --ws <id> (--turn N | --undo <restore-id>) [--dry-run]
tidebreak code git commit --ws <id> [-m MSG]
tidebreak code git push --ws <id>
tidebreak code git pr --ws <id> [--title <title>] [--body <body>]
tidebreak code git status --ws <id>
tidebreak code action <name> --ws <id>
tidebreak code watch [--once] [--timeout <secs>]Run tidebreak with no arguments, or tidebreak --help, to print this list.
To start the server, run tidebreak serve.
-p, output, attach, data, agent-mcp, plugins, the setup commands, and the
code family additionally take --server <url> / --server-token-env <var>,
--attach, or --embed — see
Which data does the CLI use? and
Attaching to a running server.
Every tidebreak code command also accepts --json (one object, or NDJSON
for run and watch). code session start takes --reasoning <level> and
--fast on the CLI; they are not HTTP-only.
code restore puts a workspace's files back to how they stood before a turn,
and undoes every change made since, including your own and other agents'.
--turn N names the turn the way code diff does. Run it with --dry-run
first to list what it would undo, including other agents' turns. It refuses
while a turn runs; while an ignored file, a nested repository, or anything
else no checkpoint holds is in the way, and the refusal names those files;
and when the checkpoint from just before that turn is gone. A restore prints
its id, and a restore that stops partway names it in its error; pass it to
--undo to put back what the restore replaced.
code git commit refuses at once while a turn runs or a message waits in the
workspace's queue, so it never commits a turn's work under your message.
The code family is a thin client of the server API, not a second product surface. These server capabilities stay HTTP-only on purpose — they are interactive desktop or agent-MCP workflows, and a CLI verb would either duplicate the desktop or invent a gate the interactive CLI does not have:
- Queued follow-ups (
list_queued_code_turnsis used byagent-mcp; there is notidebreak code queueverb) - Mid-turn steer on a code session (chat mid-turn steer is
tidebreak chat steer) - Forking a session into a transcript for another engine
- Triggers
- Auxiliary terminals
- Pull-request comments, merge, ready, and check-log download
- Delivery center
- Analytics and subscription usage
- Retry-setup
- Reverting one file or hunk from a diff, and discarding a file's uncommitted changes (review actions in the desktop's diff and source-control views)
- Worktree search, tree, and blob
- Worktree-root configuration
- Clone
code repo addsetup/archive scripts and quick actions
Code mode always drives Tidebreak's pinned harness packages through its exact
managed Node runtime. The desktop app provisions that runtime; a headless
server can reuse it when it starts with the same TIDEBREAK_DATA_DIR, but does
not download Node itself or fall back to a system installation. On a fresh
headless data directory, tidebreak code doctor reports the missing managed
runtime until that directory has been provisioned by the desktop.
serve
Starts the local API on an ephemeral loopback port and prints the address and a freshly minted per-launch bearer token:
ANTHROPIC_API_KEY=sk-... tidebreak servetidebreak: listening on http://127.0.0.1:53421
tidebreak: token 7f2c…The token is a secret. Capture the process's stdout directly rather than running it under a logging supervisor.
With TIDEBREAK_DATA_DIR unset, serve serves the Tidebreak app's own data,
so quit the app first. To serve a separate profile, set TIDEBREAK_DATA_DIR to
its folder.
The API is not exposed on a public interface. There is no generated OpenAPI
document — the route table and handler documentation in tidebreak-server are
the specification.
-p (one-shot)
Runs a single turn without a terminal. stdout carries the assistant's text, and the exit status says how the turn ended.
tidebreak -p "summarize the CSV in output/"
tidebreak -p "…" --output-format json
tidebreak -p "…" --permission-mode allow
tidebreak -p "…" --model anthropic::claude-haiku-4-5-20251001--output-format json emits the turn's event stream as NDJSON instead of plain
text. --chat <id> continues an existing conversation. --model <key> pins the
chat's model (a catalog key such as anthropic::claude-haiku-4-5-20251001)
before the turn starts; an unavailable selection fails the process instead of
falling back silently.
--permission-mode sets the chat's permission mode for the run — the same four
modes the desktop offers, applied before the turn starts:
| Mode | Effect |
|---|---|
ask | Default. Uncovered mutating calls park on an approval. |
auto | Workspace writes proceed; sensitive calls still ask. |
allow | Nothing asks. Explicit full autonomy for this chat. |
plan | Read-only exploration; the agent proposes a plan instead of acting. |
The mode is stored on the chat, so --chat <id> on a later run inherits
whatever the last run set unless it passes --permission-mode again.
Driving a turn
A turn can reach a point only someone else can settle: an approval, a proposed
plan, a question. Attach a driver — a process holding this one's stdin —
and it answers them over a line protocol. Driving is on when
--output-format json is set and stdin is not a terminal; a terminal on stdin
means a person ran the command by hand, and no decision lines are coming.
Requests are emitted on stdout, mixed into the NDJSON event stream. Every line
the CLI authors carries an tidebreak version tag, which is what tells it apart
from a journal frame (those carry seq and event):
{"tidebreak":"v1","type":"approval_request","call_id":"…","action":"exec","approval":"exec_may_run_networked_command","grant_rungs":["exact_action",{"command_prefix":{"tokens":1}}],"preview":{…}}
{"tidebreak":"v1","type":"plan_proposal","call_id":"…","title":"Rewrite the importer","plan":"1. …"}
{"tidebreak":"v1","type":"questions_asked","call_id":"…","questions":[{"id":"target","question":"Which environment?","header":"","question_type":"single","allow_free_form":true,"options":[{"id":"staging","label":"Staging"}]}]}
{"tidebreak":"v1","type":"error","message":"a plan decision does not answer the pending approval request"}
{"tidebreak":"v1","type":"halted","reason":"plan_undriven","exit_code":3,"call_id":"…","message":"…"}The driver answers by writing one JSON object per line to stdin:
{"type":"approval","call_id":"…","decision":"approve","grant":"exact_action"}
{"type":"approval","call_id":"…","decision":"reject","reason":"not in this run"}
{"type":"plan","call_id":"…","decision":"accept","permission_mode":"auto"}
{"type":"plan","call_id":"…","decision":"reject","feedback":"split it in two"}
{"type":"questions","call_id":"…","answers":[{"question_id":"target","selected_option_ids":["staging"],"custom_answer":"canary first"}]}call_id may be omitted — only one interaction is ever outstanding — but
naming a different call is an error, not a redirect. reason (approval
rejections), feedback and permission_mode (plan), grant (approval), and
custom_answer are optional. Unknown fields are ignored so a newer driver can
send more; an unknown type or verdict is refused with an error event and
the next line is read, so one malformed line never decides anything by itself.
Undriven defaults
With no driver — text output, a terminal on stdin, or an input stream that ends without answering — the standing policy applies:
- Approvals are rejected, with the reason
non-interactive print mode. The model sees the rejection and can choose another route rather than hang. - Plans and questions end the run. There is no answer to invent, so the
turn is cancelled and reported: a
haltedobject naming the reason, and a distinct exit status.
The halted object goes to stdout under --output-format json and to stderr
under text output, so an undriven run is always machine-diagnosable.
Exit codes
| Code | Meaning |
|---|---|
0 | The turn completed. |
1 | The turn failed, was refused, or was cancelled. |
2 | Usage error — a bad flag or argument. |
3 | The turn parked on a plan or question and no driver answered. reason is plan_undriven or questions_undriven. |
4 | Something the run had to settle could not be carried through. reason is decision_failed when a decision was made and the server refused it, pending_lookup_failed when the parked request itself could not be read, or folder_decline_failed when a request_folder_access call was left parked and the refusal it needs could not be delivered. |
130 | Interrupted (SIGINT). The turn is cancelled, and a halted object names the reason interrupted. |
tidebreak code run and tidebreak code watch add two more codes
(crates/tidebreak-cli/src/code.rs):
| Code | Meaning |
|---|---|
3 | --on-approval fail saw a parked approval. |
124 | Timed out waiting for a turn or a watch snapshot (--timeout). Same number GNU timeout(1) uses. |
130 | Interrupted (SIGINT), same convention as -p. |
A dev harness
To drive Tidebreak from a script or a coding agent, give it its own data directory so it cannot touch the desktop's chats or credentials, and let it act without parking:
export TIDEBREAK_DATA_DIR="$(mktemp -d)/tidebreak"
export TIDEBREAK_KEYCHAIN_MOCK=1 # debug builds only — see below
export ANTHROPIC_API_KEY=sk-...
tidebreak -p "run the tests and summarize what failed" \
--permission-mode allow --output-format json < /dev/nullTIDEBREAK_KEYCHAIN_MOCK=1 keeps credentials in memory instead of the OS
keychain, which is what stops a headless run prompting for keychain access. It
is honored by debug builds only; a release binary always uses the real
keychain. Redirecting stdin from /dev/null states plainly that nothing is
driving, so the run takes the undriven defaults instead of waiting.
To drive it instead, hold both pipes — read stdout line by line, and write a decision line when a request arrives:
tidebreak -p "plan the migration" \
--permission-mode plan --output-format json < decisions.pipeSetup commands
provider, model, settings, mcp-server, plugins, and chat configure a profile
without the desktop window. Each one reaches the same server the other commands
do — the running app, or one it starts in its own process — calls the route
the desktop's settings pages call, and prints the answer. They hold no
configuration logic of their own, so both surfaces stay in step. All of them
accept --output-format text|json, and json prints one object on one line,
stamped with schema_version (see Compatibility).
# Providers and credentials
tidebreak provider list
tidebreak provider set-key anthropic < key.txt
tidebreak provider set-key anthropic --from-env ANTHROPIC_API_KEY
tidebreak provider remove-key anthropic
# Models
tidebreak model list
tidebreak model roles
tidebreak model select anthropic::claude-opus-5
tidebreak model select auto --role utility
# Web search and code execution
tidebreak settings show
tidebreak settings web-search select exa
tidebreak settings web-search set-key exa --from-env EXA_KEY
tidebreak settings exec select e2b
tidebreak settings exec set-key e2b --from-env E2B_KEY
# MCP servers
tidebreak mcp-server list
tidebreak mcp-server add docs \
--command npx --arg -y --arg @acme/docs-mcp --env-from ACME_TOKEN
tidebreak mcp-server add vercel \
--url https://mcp.vercel.com --oauth
tidebreak mcp-server remove docs
# Plugins from a pinned Git source
tidebreak plugins install \
--git https://github.com/acme/notes --ref v1.0.0
tidebreak plugins install \
--git https://github.com/acme/notes --ref v1.0.0 --json
# Chats
tidebreak chat list
CHAT=$(tidebreak chat create)
tidebreak -p "…" --chat "$CHAT"
tidebreak chat retry "$CHAT" --wait
tidebreak chat regenerate "$CHAT" --wait
tidebreak chat regenerate "$CHAT" --model anthropic::claude-opus-5
tidebreak chat edit "$CHAT" "Make it shorter" --wait
BRANCH=$(tidebreak chat branch "$CHAT")chat retry, chat regenerate, chat edit, and chat branch rerun and
branch a conversation the way the desktop's message actions do. Each acts on
the conversation's latest turn unless you pass --turn.
retrycontinues the latest turn after it failed or was stopped. The model sees everything that turn said and every tool call it made, so it does not repeat work that already ran. A turn that finished is regenerated instead.regenerateanswers the latest message again. The earlier answer stays as a version you can page back to in the app.--modelanswers with another model this once; the conversation keeps its own model.editreplaces the latest message and answers the new one.branchstarts a new conversation with a copy of the history through the turn, named after the original and linked back to it. It prints the new conversation's id.
When the answer that regenerate or edit replaces did anything but read,
such as writing files, running commands, or calling connected apps, it starts
a new conversation instead, and says so on stderr. The same holds when an
earlier answer to the same message did. The original conversation keeps its
record of what ran.
Without --wait, retry, regenerate, and edit print the new turn's id and
return once the turn is accepted. With --wait, they print the answer, and exit 1
when the turn fails or is stopped. --output-format json adds chat, turn,
replaces, branched, and side_effects, and with --wait, status and
answer.
A key is never a command-line argument. set-key reads it from stdin, or
from the environment variable named by --from-env. Arguments are visible to
every process on the machine through the process list and land in shell
history; a pipe and an exported variable do not. Stored keys go to the OS
keychain entry of the profile the command works on. The app's profile keeps
the entry the app uses. A profile named by TIDEBREAK_DATA_DIR has an entry of
its own, under the keychain service tidebreak.profile.<id>
(tidebreak.dev.profile.<id> for a debug build), where <id> comes from the
folder's path. No command here ever prints a key back.
Storing a provider key also enables that provider for routing, matching what
saving a key in the app does. model select writes the same role selections
the settings page does: a catalog key pins the role, and auto returns it to
automatic resolution.
settings show reports the runtime settings alongside web-search and
code-execution selection, availability, and which credential slots are filled.
mcp-server add and remove edit the user-configured server set. Servers
that come from an installed plugin appear in list and are managed by turning
that plugin on or off — the server rebuilds them, so the config route refuses
to edit them. --command (with repeatable --arg), --url, and
--gateway-endpoint are the three transports, and exactly one is required.
Secret material is named, not given: --env-from forwards a variable from the
parent environment, and --bearer-token-env names the variable holding a
remote server's token. Both are resolved when the server connects. --oauth
marks a --url server that signs in with OAuth instead; connect it from
Settings, which opens the sign-in page in a browser.
output
A turn's files land as conversation outputs. These read them, and write one to a path — the same routes the desktop's Outputs panel uses, so a file produced in the app is readable here and the other way round.
tidebreak output list <chat-uuid>
tidebreak output show <chat-uuid> <output-uuid>
tidebreak output revisions <chat-uuid> <output-uuid>
tidebreak output export <chat-uuid> <output-uuid> ./report.mdlist prints one tab-separated row per output, starting with the id the other
subcommands take. show writes the text preview to stdout and names the
revision it read on stderr, so tidebreak output show … > draft.md is exactly
the text. revisions lists an output's version history — every version keeps
its own id and bytes, including after a restore, because history is
append-only. export writes the complete bytes of a revision, which is the
only way to get a binary artifact (a chart, a spreadsheet) out.
--revision <id> names an exact version instead of the current one on show
and export.
attach
Puts a local file into a conversation, the same way the app's attach button does. What the bytes are decides where they go: an image is published as an image attachment, everything else is ingested as a source document. The id it prints on stdout is what a later turn references.
tidebreak attach <chat-uuid> ./contract.pdfdata
The same backup and export the desktop's Settings → Data and privacy page offers, through the same routes.
tidebreak data show
tidebreak data backup ./tidebreak-backup.tar.gz
tidebreak data export ./conversations.zip
tidebreak data export ./chat.json --format json --chat <chat-uuid>show prints the data folder and how much disk its database, attachments,
outputs, logs, engine tools, and backups use.
backup writes the data folder as one .tar.gz: the database, the files
attached to conversations, the files Tidebreak made, skills, plugins, folder
permissions, and coding session files. It copies the database with SQLite's own
VACUUM INTO while the server keeps running, so the copy is consistent without
stopping anything. It leaves out keys, which stay in the keychain, and logs,
engine tools, earlier backups, working files, and worktrees from before version
0.59. tidebreak-backup.json in the archive lists both. A server on PostgreSQL
refuses backup: back that database up with its own tools, as
Self-hosting describes.
To restore a backup, quit Tidebreak and rename the data folder; do not delete it. Create an empty folder with the old name, extract the archive into it, and copy back anything the archive leaves out that you still want.
export writes your chats: a .zip with one Markdown file per chat, or one
JSON document with --format json. Each holds the messages and the names of
attached files. Coding sessions, tool activity, and attachment contents are not
in it. Each --chat limits the export to that chat.
Neither command replaces a file already at the path unless you pass --force.
Both write the file in full before they put it at the path.
mcp
Serves Tidebreak's own read-only filesystem tools over MCP stdio, confined to one explicit workspace directory:
tidebreak mcp /absolute/path/to/workspaceIt exposes read_file and list_dir, and only after a proper MCP initialize
handshake. This is Tidebreak acting as an MCP server for some other client —
it is unrelated to connecting MCP servers to Tidebreak.
Agent MCP
tidebreak agent-mcp is the other direction: an MCP server that lets an
external agent drive chat on a Tidebreak that is already running. It is an
attach client, the same way -p is. mcp and browser-mcp refuse the
connection flags; this command takes them, because its tools talk to the
attach contract, not to a workspace directory.
With no flags, it connects to the Tidebreak app, so an MCP client can launch
it as a bare command and drive the chats you see in the app. It never starts a
server of its own unless you pass --embed, even when TIDEBREAK_DATA_DIR is
set: pass --attach to connect to the server that owns that folder, or
--server <url> with TIDEBREAK_SERVER_TOKEN for another server.
# The app on this computer
tidebreak agent-mcp
# Another server
export TIDEBREAK_SERVER_TOKEN=<the token that server printed>
tidebreak agent-mcp --server http://127.0.0.1:53421stdout is JSON-RPC only. Diagnostics go to stderr.
The tools are schema-discoverable (tools/list). Reads are read-only;
mutations are sensitive. Possession of the bearer is what authorizes calling
them — the driven chat's own approvals are never auto-approved.
| Tool | What it does |
|---|---|
chat_create | Create a chat. Optional model and permission_mode (plan, ask, auto, allow). Returns {chat_id}. |
chat_list | Chat summaries. |
chat_status | Run state, a tail of recent messages, pending approvals / plans / questions. |
chat_run_turn | Post a prompt with a fresh turn id and follow until the turn settles, parks, or timeout_seconds (default 300) elapses. |
chat_wait | Re-follow an in-flight turn. Same return shape as chat_run_turn. |
chat_decide | Apply a print-protocol decision, then follow to the next settle point. |
chat_events | Raw journal frames after after_seq, at most max_events (capped at 200). |
chat_steer | Inject more user text into the in-flight turn. |
chat_cancel | Cancel the in-flight turn. |
chat_run_turn, chat_wait, and chat_decide return the same object:
{
"status": "completed",
"assistant_text": "…",
"pending": null,
"events_cursor": 42
}status is one of completed, needs_approval, needs_plan_decision,
needs_answer, needs_host_consent, running, cancelled, failed.
running means the timeout elapsed while the turn continues on the server —
turns are durable — so you re-check with chat_wait or chat_status.
The loop is run-turn, then decide, until completed (or failed /
cancelled):
{"type":"approval","call_id":"…","decision":"approve"}
{"type":"plan","call_id":"…","decision":"accept","permission_mode":"auto"}
{"type":"questions","call_id":"…","answers":[{"question_id":"target","selected_option_ids":["staging"]}]}Those objects are the same ones -p --output-format json reads on stdin. Host
folder consent is not in that vocabulary: if a turn parks on
request_folder_access, status is needs_host_consent and there is no
decision path. Standing consent still comes from tidebreak folder connect
or the desktop.
Profile and settings tools inspect and steer the axes a chat turn depends on:
| Tool | What it does |
|---|---|
profile_snapshot | One document: settings, providers (id / kind / has_credential only), the model catalog and roles, web-search config, and exec config. |
model_role_set | Pin the chat or utility role to a catalog key (auto clears it). |
web_search_select | Select the host web-search provider (off / null turns it off). |
exec_select | Select the code-execution backend (off / null disables it). |
chat_set_model | Pin a catalog model on one chat. Omit model or pass null to clear the override. |
chat_set_permission_mode | Set a chat's permission mode to plan, ask, auto, or allow. |
chat_attach_file | Attach a local file. Image extensions go through the image route; everything else is ingested as a document. Returns {id}. |
chat_outputs | List the conversation's live outputs. |
chat_output_read | Read one output. Text comes back as text; binary outputs return base64 bytes. |
agent_runs | List background agent runs for a chat. |
agent_run_cancel | Ask a background run to stop. |
Credentials do not transit this surface. There is no tool for provider, web-search, or exec keys (set-key / remove-key), and no tool for MCP server config (put_mcp_servers). A human sets those in the desktop or the CLI. profile_snapshot strips credential material, key fragments, and secret-bearing fields before it returns.
Code mode
The same agent-mcp process also drives code mode on that server: repos,
workspaces, sessions, turns, approvals, diffs, and git/PR actions. Reads are
read-only; mutations are sensitive. Possession of the bearer authorizes
calling them. Harness approvals the engine raises inside a session are never
auto-approved.
Session lifecycle:
| Tool | What it does |
|---|---|
code_harnesses | Harness doctor report. |
code_repo_add | Register a local git checkout (source, optional name). |
code_repos | Registered repos. |
code_workspace_create | Create a worktree + branch (repo_id, optional name, base). |
code_workspaces | Workspaces, optionally filtered by repo_id. |
code_workspace_archive | Archive a workspace. |
code_session_create | Start a session (workspace_id, optional harness, model, permission_mode). |
code_sessions | Sessions in a workspace. |
code_session_set_permission_mode | Change the session's permission mode. |
code_turns | Turn history plus the durable session queue. |
code_interrupt | Interrupt the in-flight turn. |
code_diff / code_files | Workspace diff and changed-file list. |
code_git_status / code_git_commit / code_git_push / code_git_pr | Git and pull-request actions. These are Sensitive tools, not a second approval gate: the bearer already authorizes the workspace. |
The run-turn → decide loop is the chat loop with a code-shaped decision:
{"approval_id":"…","decision":"approve"}
{"approval_id":"…","decision":"deny","feedback":"use the fixtures directory"}code_run_turn subscribes the session event socket, then submits. Follow until
the turn settles, parks on an approval, or timeout_seconds (default 300)
elapses. code_wait re-follows an in-flight or queued turn. code_decide
applies the decision and follows to the next settle point. code_approvals
lists what is parked.
status is the chat set plus queued. POST /sessions/{id}/turns
answers as soon as the message is accepted: with the started turn, or with a
receipt for a message parked on the durable session queue. A queued receipt
carries the queue position and the turn id the promoted turn will run under —
it is not a failure. Re-check with code_wait; one wait drains the running
turn and then the queued follow-up.
run_action and reap_session are not mounted.
rehome-secrets
macOS ties keychain approvals to a binary's code signature, so credentials stored by an earlier build keep prompting. This rewrites each stored credential under the running binary's signature.
tidebreak rehome-secretsAttaching to a running server
One data directory, one server. A server claims its data directory when it starts and holds the claim for as long as the process lives. A second process that tries to start its own server over the same directory is refused outright, because two engines writing one database race each other in ways nothing else would catch:
tidebreak: configuration error: another Tidebreak process is already running on
the data directory /Users/me/tidebreak-profile. Quit that process and try again …The claim is an OS advisory lock on tidebreak.lock inside the directory, so it
is honest across a crash: a killed process releases it the moment it dies, and
the leftover file never has to be cleaned up by hand.
The way in is to attach — to be a client of the server that is already there
rather than a second one. With TIDEBREAK_DATA_DIR unset, a command does this
on its own: it connects to the running app. For another data directory, prefer
--attach, which reads the listen.json the running server wrote into the
data directory (mode 0600, bearer only — never the native executor
credential):
# Same data directory the desktop or `serve` owns
export TIDEBREAK_DATA_DIR="$HOME/Library/Application Support/io.brightwave.tidebreak"
tidebreak --attach provider list
tidebreak -p "what changed today?" --attachThe dev app, which a debug build of the CLI uses, keeps its data in
io.brightwave.tidebreak.dev. With TIDEBREAK_DATA_DIR unset, --attach
reads the app's own listen.json, so it fails when the app is not running.
--attach, --server, and --embed conflict; pick one.
--attach believes a listen.json only while a live process holds the lock
on its data directory, and only when it names a server on this computer. The
file outlives a server that crashed or was killed, so a file whose directory
nobody holds is refused rather than followed. A reconnect after a dropped
connection reads the file again under the same checks.
You can still pass an explicit URL when you already have the pair (for example
from serve's stdout):
export TIDEBREAK_SERVER_TOKEN=<the token that server printed>
tidebreak --server http://127.0.0.1:53421 provider list--server, --attach, and --embed work on -p, agent-mcp, and every setup command.
TIDEBREAK_SERVER_URL sets the same thing as --server for a whole shell, and
the flag wins over it. After the endpoint is resolved, an attached command
writes no log file and touches no keychain — it is only an HTTP+WebSocket
client. With --attach, TIDEBREAK_DATA_DIR is used solely to find
listen.json; with --server, the local data directory is ignored.
serve, mcp, and rehome-secrets reject --server, --attach, and
--embed: the first two are servers, and the third rewrites local keychain
items. (They ignore TIDEBREAK_SERVER_URL, so a shell that exports it can
still start a daemon.)
An attached command checks the server's version before it does anything else. See The version check.
Getting a token
The bearer token is per-launch and is full authority over the profile, so it is
never a command-line argument — every process on the machine can read those, and
they land in shell history. --attach loads it from listen.json. With
--server it comes from TIDEBREAK_SERVER_TOKEN, or from the variable
--server-token-env <var> names.
From tidebreak serve or the desktop app: both write
{data_dir}/listen.json on bind. Point TIDEBREAK_DATA_DIR at that directory
and use --attach, or leave it unset to reach the app. serve still prints
the URL and token on stdout if you prefer to capture them:
TIDEBREAK_DATA_DIR="$(mktemp -d)" tidebreak serve > server.log &
export TIDEBREAK_SERVER_URL=http://$(grep -m1 'listening on http://' server.log | sed 's|.*http://||')
export TIDEBREAK_SERVER_TOKEN=$(grep -m1 'token ' server.log | sed 's|.*token ||')Compatibility
What 1.x keeps stable
A 1.x release keeps these compatible. A change that would break one waits for a major release.
- Commands and flags. Every command and flag in the Commands block. A release can add a command or a flag.
- Which data a command uses. The rules in
Which data does the CLI use?: the app's
profile when
TIDEBREAK_DATA_DIRis unset, the named folder when it is set, and never the folder a command runs from. - Exit codes. Every command exits
0on success,1on failure, and2on a usage error.-pand thecodefamily add the codes in the tables above. - JSON documents. Apart from the event streams listed under
What is internal, a command run with
--output-format jsonor--jsonprints one object on one line, and every object carries"schema_version": 1. The fields the CLI writes itself keep their names, types, and meaning. New fields can appear, so ignore fields you do not know. - The
-pdecision protocol. The"tidebreak":"v1"lines print mode writes to stdout and the decision lines it reads from stdin, as Driving a turn describes them. agent-mcp. Its tool names, their input schemas, and the result shape (status,assistant_text,pending,events_cursor). New tools and new optional inputs can appear.
Many JSON documents also carry a record as the server returned it, such as
the snapshot a code command prints, the MCP server listing, or the objects
inside settings show. The fields of such a record come from the HTTP API
below, which is internal, so they are not part of this promise.
Tests check the commands and flags against the CLI's usage text and the exit
codes against the code. Others run a set of commands and check each document's
version and top-level keys, and pin the agent-mcp tool names and the -p
protocol lines.
What is internal
These can change in any release. If you build on one, pin the Tidebreak version you tested against:
- The HTTP routes and their JSON bodies.
- The chat and code event wire, including every WebSocket frame.
- The event frames in the NDJSON streams from
-p --output-format json,code run --json, andcode watch --json. In the-pstream, key on the"tidebreak":"v1"lines, not on the frames.
The version check
Every attached command reads GET /version first. The server answers without
a bearer, with its release and the API level it serves:
{"version":"1.3.0","api_level":1}/healthz and /auth/discovery carry the same two keys. The CLI compares
api_level with the range of levels it reads. If the server is newer than
that range, the command stops before it runs anything:
tidebreak: This server runs Tidebreak 1.3.0. Update Tidebreak to 1.3.0 or later to connect.If the server is older than the range, the message asks you to update the
server instead. A server from before the check answers /version with 404,
and the CLI attaches to it as before. The desktop app and the mobile app run
the same check when they attach to a machine.
The API level is not the release number. It rises only when a change breaks older clients, such as a removed or renamed field. A change that adds a field, an event, or a route leaves it alone, because Tidebreak's clients ignore keys they do not know. When a newer server sends an event the CLI cannot read, the CLI skips that event, keeps following the turn, and says so on stderr.
Environment
| Variable | Effect |
|---|---|
TIDEBREAK_DATA_DIR | Where the database, blobs, logs, and listen.json live. Unset: the Tidebreak app's own data directory, and a client command connects to the app instead of starting a server; a debug build uses the dev app's. Nothing defaults to the current directory. With --attach, used only to locate listen.json; with --server, ignored. Required for self_host. |
TIDEBREAK_SERVER_URL | Attach to the server at this base URL instead of embedding one. --server <url> overrides it; conflicts with --attach and --embed. serve/mcp/rehome-secrets ignore it. |
TIDEBREAK_SERVER_TOKEN | Bearer token for --server, unless --server-token-env <var> names a different variable. Not used with --attach. |
TIDEBREAK_PROFILE | desktop (default, SQLite) or self_host (PostgreSQL). |
TIDEBREAK_DATABASE_URL | Required for self_host. |
TIDEBREAK_AUTH_TOKENS_FILE | Named bearer tokens for self_host. Required there. |
TIDEBREAK_VAULT_ADDR | Vault base URL for stored self-host credentials. Requires TIDEBREAK_VAULT_TOKEN_FILE; HTTPS is required except for literal loopback development. |
TIDEBREAK_VAULT_TOKEN_FILE | Mounted Vault token file. Tidebreak reads it for every request so rotation does not require a restart. |
TIDEBREAK_VAULT_MOUNT | Optional KV v2 mount path. Defaults to secret. |
TIDEBREAK_VAULT_PATH | Optional deployment path below the mount. Defaults to tidebreak. |
TIDEBREAK_VAULT_NAMESPACE | Optional Vault Enterprise or HCP namespace. |
TIDEBREAK_SECRET_KEY_FILE | File holding the 32-byte base64 key that encrypts stored self-host credentials in the database. Use it or the Vault variables, not both. |
TIDEBREAK_MCP_CONFIG | JSON file of external MCP server definitions to load at boot. |
TIDEBREAK_KEYCHAIN_MOCK | Debug builds only. Any non-empty value routes credentials to an in-memory store instead of the OS keychain, so a headless run never triggers a keychain prompt. Secrets do not survive the process. Release binaries ignore it. |
TIDEBREAK_MODEL | Overrides the model the process launches with. |
TIDEBREAK_CONTAINER_EXECUTION_ENABLED | Enables the container execution backend. Default false. |
TIDEBREAK_CONTAINER_IMAGE | Overrides the container image. |
ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, GEMINI_API_KEY, FIREWORKS_API_KEY, TOGETHER_API_KEY | Used as a credential for that provider when nothing is stored in the credential store. |
With TIDEBREAK_DATA_DIR unset, the CLI works on the Tidebreak app's data:
commands connect to the app while it runs, and serve and --embed open the
same data only while it does not, because a data directory has one server at a
time. To keep a script's chats and credentials apart from the app's, set
TIDEBREAK_DATA_DIR to a folder of its own. See
Which data does the CLI use?.
Logs are written to logs/tidebreak.log under the data directory.
Self-hosting
A self_host profile backed by PostgreSQL exists. It authenticates with named
bearer tokens from an operator file, and every chat, project, and document
query is scoped to the authenticated principal.
One thing to know before using it: settings are deployment-scoped, not
per-user. Enabled providers, credentials, model roles, and web-search and
code-execution configuration are shared by the deployment. Only an admin
token can change them; a member token receives 403 on that plane and keeps
its own chats, projects, documents, and event stream.
What a member runs is this API and the CLI with --server and a token from
the operator file. The desktop app is the local Desktop profile and does not
connect to a remote self-host deployment. The profile is for mutually trusting
users of one operator's deployment — a household or a small team. It is not
multi-tenant isolation.
Stored credentials are encrypted in the database when
TIDEBREAK_SECRET_KEY_FILE names a key file, or kept in HashiCorp Vault KV v2
when TIDEBREAK_VAULT_ADDR and TIDEBREAK_VAULT_TOKEN_FILE are set. With
neither, stored-secret reads return unset so provider environment variables
remain fallbacks, while writes and deletes fail with setup guidance. The
self-host profile never opens the desktop OS keychain.
Multi-process ownership is not finished. Self-host blob bytes live in the
local directory or the S3-compatible bucket named by
TIDEBREAK_BLOB_STORE_URL. Treat self-hosting as experimental.
Build from source
If you work on Tidebreak itself, you can run the CLI from a clone instead of the installed command. To build a release binary, run:
cargo build -p tidebreak-cli --releaseTo run the CLI without installing it, put cargo run -p tidebreak-cli -- in
place of tidebreak in any example above:
cargo run -p tidebreak-cli -- <args>A debug build, such as cargo run -p tidebreak-cli --, uses the dev app's
profile (io.brightwave.tidebreak.dev) in place of the installed app's. A
release build uses the installed app's profile.