TidebreakDocs

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:

  1. In the Tidebreak menu, choose Install the tidebreak Command. You can also open Settings → Command line and choose Install the tidebreak command.

  2. Tidebreak links tidebreak into ~/.local/bin, creates that folder if it does not exist, and says whether the folder is on your PATH.

  3. 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 --version

To 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_DIR unset. 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 --embed to work on the app's data without opening the app: the command then runs the server in its own process.
  • TIDEBREAK_DATA_DIR set. The CLI uses the profile in that folder, and a command runs its own server over it. Add --attach to 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-secrets

It 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_turns is used by agent-mcp; there is no tidebreak code queue verb)
  • 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 add setup/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 serve
tidebreak: 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:

ModeEffect
askDefault. Uncovered mutating calls park on an approval.
autoWorkspace writes proceed; sensitive calls still ask.
allowNothing asks. Explicit full autonomy for this chat.
planRead-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 halted object 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

CodeMeaning
0The turn completed.
1The turn failed, was refused, or was cancelled.
2Usage error — a bad flag or argument.
3The turn parked on a plan or question and no driver answered. reason is plan_undriven or questions_undriven.
4Something 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.
130Interrupted (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):

CodeMeaning
3--on-approval fail saw a parked approval.
124Timed out waiting for a turn or a watch snapshot (--timeout). Same number GNU timeout(1) uses.
130Interrupted (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/null

TIDEBREAK_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.pipe

Setup 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.

  • retry continues 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.
  • regenerate answers the latest message again. The earlier answer stays as a version you can page back to in the app. --model answers with another model this once; the conversation keeps its own model.
  • edit replaces the latest message and answers the new one.
  • branch starts 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.md

list 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.pdf

data

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/workspace

It 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:53421

stdout 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.

ToolWhat it does
chat_createCreate a chat. Optional model and permission_mode (plan, ask, auto, allow). Returns {chat_id}.
chat_listChat summaries.
chat_statusRun state, a tail of recent messages, pending approvals / plans / questions.
chat_run_turnPost a prompt with a fresh turn id and follow until the turn settles, parks, or timeout_seconds (default 300) elapses.
chat_waitRe-follow an in-flight turn. Same return shape as chat_run_turn.
chat_decideApply a print-protocol decision, then follow to the next settle point.
chat_eventsRaw journal frames after after_seq, at most max_events (capped at 200).
chat_steerInject more user text into the in-flight turn.
chat_cancelCancel 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:

ToolWhat it does
profile_snapshotOne document: settings, providers (id / kind / has_credential only), the model catalog and roles, web-search config, and exec config.
model_role_setPin the chat or utility role to a catalog key (auto clears it).
web_search_selectSelect the host web-search provider (off / null turns it off).
exec_selectSelect the code-execution backend (off / null disables it).
chat_set_modelPin a catalog model on one chat. Omit model or pass null to clear the override.
chat_set_permission_modeSet a chat's permission mode to plan, ask, auto, or allow.
chat_attach_fileAttach a local file. Image extensions go through the image route; everything else is ingested as a document. Returns {id}.
chat_outputsList the conversation's live outputs.
chat_output_readRead one output. Text comes back as text; binary outputs return base64 bytes.
agent_runsList background agent runs for a chat.
agent_run_cancelAsk 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:

ToolWhat it does
code_harnessesHarness doctor report.
code_repo_addRegister a local git checkout (source, optional name).
code_reposRegistered repos.
code_workspace_createCreate a worktree + branch (repo_id, optional name, base).
code_workspacesWorkspaces, optionally filtered by repo_id.
code_workspace_archiveArchive a workspace.
code_session_createStart a session (workspace_id, optional harness, model, permission_mode).
code_sessionsSessions in a workspace.
code_session_set_permission_modeChange the session's permission mode.
code_turnsTurn history plus the durable session queue.
code_interruptInterrupt the in-flight turn.
code_diff / code_filesWorkspace diff and changed-file list.
code_git_status / code_git_commit / code_git_push / code_git_prGit 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-secrets

Attaching 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?" --attach

The 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_DIR is unset, the named folder when it is set, and never the folder a command runs from.
  • Exit codes. Every command exits 0 on success, 1 on failure, and 2 on a usage error. -p and the code family 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 json or --json prints 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 -p decision 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, and code watch --json. In the -p stream, 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

VariableEffect
TIDEBREAK_DATA_DIRWhere 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_URLAttach 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_TOKENBearer token for --server, unless --server-token-env <var> names a different variable. Not used with --attach.
TIDEBREAK_PROFILEdesktop (default, SQLite) or self_host (PostgreSQL).
TIDEBREAK_DATABASE_URLRequired for self_host.
TIDEBREAK_AUTH_TOKENS_FILENamed bearer tokens for self_host. Required there.
TIDEBREAK_VAULT_ADDRVault base URL for stored self-host credentials. Requires TIDEBREAK_VAULT_TOKEN_FILE; HTTPS is required except for literal loopback development.
TIDEBREAK_VAULT_TOKEN_FILEMounted Vault token file. Tidebreak reads it for every request so rotation does not require a restart.
TIDEBREAK_VAULT_MOUNTOptional KV v2 mount path. Defaults to secret.
TIDEBREAK_VAULT_PATHOptional deployment path below the mount. Defaults to tidebreak.
TIDEBREAK_VAULT_NAMESPACEOptional Vault Enterprise or HCP namespace.
TIDEBREAK_SECRET_KEY_FILEFile 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_CONFIGJSON file of external MCP server definitions to load at boot.
TIDEBREAK_KEYCHAIN_MOCKDebug 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_MODELOverrides the model the process launches with.
TIDEBREAK_CONTAINER_EXECUTION_ENABLEDEnables the container execution backend. Default false.
TIDEBREAK_CONTAINER_IMAGEOverrides the container image.
ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, GEMINI_API_KEY, FIREWORKS_API_KEY, TOGETHER_API_KEYUsed 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 --release

To 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.

On this page