Reference
Configuration
conch keeps its socket, state and settings in one directory. Most settings can be changed from the Settings screen (,), which writes config.toml for you.
Files
Everything lives in ~/.config/conch. Set CONCH_HOME to use another directory; without it, conch uses $XDG_CONFIG_HOME/conch when XDG_CONFIG_HOME is set.
| File | Contents |
|---|---|
conch.sock | the server's socket (override with CONCH_SOCKET) |
server.log | the server's log |
projects.json | the projects in the sidebar |
claude-settings.json | the hooks conch passes to Claude Code with --settings |
ui.json | fold state, sidebar width, tabs and splits, saved ssh hosts |
machines.json | remote machines: only an ID, label and ssh target |
phone.json | the phones paired with conch web: a name, a permission and a hash of each one's token, their push subscriptions, and the key pushes are signed with |
ssh/config | generated ssh config |
binaries/<os>-<arch>/conch | conch builds for other platforms |
agents/claude.toml | optional override of Claude's screen rules |
config.toml | your settings |
Environment variables
| Variable | Effect |
|---|---|
CONCH_HOME | the directory above |
CONCH_SOCKET | the server socket path |
CONCH_SOURCE | conch's source tree, for cross-compiling to remote machines |
CONCH_REMOTE_BINARY | a prebuilt conch binary to install on remote machines |
CONCH_VERSION, CONCH_INSTALL_DIR | release and directory for the install script |
DAYTONA_API_KEY, BOAT_API_KEY, DEVIN_API_KEY | the key for sandboxes, one per provider |
DAYTONA_API_URL, DAYTONA_TARGET | Daytona's API and region, when [sandbox.daytona] doesn't set them |
BOAT_API_URL | boat.dev's API, when [sandbox.boat] doesn't set it |
DEVIN_API_URL | Devin's API, when [sandbox.devin] doesn't set it |
DEVIN_ORG_ID | the Devin organization sessions are made in; without it conch asks Devin which one the key's sessions run in |
CONCH_DEVIN | the devin CLI conch reaches a Devin Cloud session with; without it, devin on PATH, then ~/.local/bin/devin |
CONCH_LOG_CALLER | 1 writes a line per connection saying which pane the server placed it in, with the process ancestry it climbed — for finding out why an agent that should be scoped is not |
CONCH_STATUS | 0 stops conch telling the terminal what its agents are doing, 1 makes it do so where it otherwise would not |
config.toml
A config.toml conch cannot read never keeps you out: it starts on the defaults and says what was wrong with the file, in the TUI as a message on start-up and on the terminal for conch ask. Saving from the Settings screen then keeps the file that could not be read as config.toml.invalid, so a half-finished edit is never written over.
All keys with their defaults:
[keys]
prefix = "ctrl+b"
[pane]
default_command = "" # empty = $SHELL
[notify]
enabled = true
desktop = true # macOS notification / notify-send
sound = false # system sound
bell = false # terminal beep
waiting = true # when an agent needs an answer
done = true # when an agent finishes
quiet_start = "" # quiet hours, e.g. "22:00"
quiet_end = "" # e.g. "08:00"
limits = true # when a Claude or Codex plan limit window is nearly used
limit_at = [80, 95] # percent used that alerts
silence = 30 # seconds without output before a watched pane (ctrl+b M) alerts
command = "" # also run for every alert, with it as JSON on stdin: ntfy, Slack, Telegram…
[git]
squash = true # m → Merge puts one commit on the base rather than a merge commit (auto_merge is GitHub's, and follows the repository)
[verify]
commands = {} # a project's check, by project ID: { r1a2b3c = "go test ./..." }
auto_fix = 0 # send a failed check — the project's own, or a pull request's CI — back to the branch's agent up to this many times in a row
auto_merge = false # merge a pull request on GitHub when its checks turn green (held while a draft, conflicting, under review or an unresolved thread, or past a failed check)
auto_pr = false # push a branch whose agent finished and whose check passed, and open its pull request (or push to the open one)
auto_resolve = 0 # rebase a pull request that conflicts with its base, asking the branch's agent up to this many times when that stops on a conflict
require_tests = false # open a pull request whose branch changes code and no test as a draft (auto_merge leaves drafts), and ask the agent for a test
max_diff = 0 # open one bigger than this many changed lines as a draft too; 0 is no limit
# both are handed to each machine's server, which acts on them with conch closed
reviewer = "" # an agent asked to review a branch when its agent finishes, e.g. "codex"
[ui]
mouse = true
theme = "conch" # conch, dracula, catppuccin, nord, gruvbox, tokyo-night, one-dark, solarized,
# rose-pine, kanagawa, everforest, monokai, github-dark, ayu, night-owl
accent = "" # override: teal, blue, green, orange, pink, red, gray, purple or "#rrggbb"
cost = true # show what agents spend in the tree, Sessions and the pages
icons = "text" # file explorer icons: text (letters, any font), nerd (Nerd Font glyphs) or off
hover = false # tint the row or tab under the pointer (every move is reported)
tree_groups = "sections" # a project's panes in the tree: sections (Agents and Terminals), or tabs
diff = "auto" # how a file's changes are read: auto (two columns where there is room),
# side (always two; long lines wrap in their column) or unified (always one).
# s in the changes view overrides it for the file being read
[shell]
omz_theme = "" # Oh My Zsh theme for new zsh terminals; "" keeps .zshrc's
[agents]
default = "claude" # what c starts: claude, codex, gemini, opencode
permission_mode = "on" # [[permissions]] rules: on, dry-run (record only) or off
[brain]
provider = "claude" # claude, anthropic or openai
model = ""
summary_model = ""
summaries = false
[update]
check_releases = true # ask GitHub once a day for a newer release
auto = false # and install it as soon as it is found
[remote]
upload_drops = true # upload files dropped on a remote pane and paste their paths there
upload_max_mb = 25 # refuse larger files (the server takes at most 256)
[sandbox.daytona] # one table per provider ([sandbox.boat] too); the same keys for each
api_key = "" # the key itself, kept in this file; "" reads it from the environment
api_key_env = "" # variable holding the key; "" is DAYTONA_API_KEY
api_url = "" # "" is $DAYTONA_API_URL or the provider's own
target = "" # region, e.g. "us" or "eu"
snapshot = "" # what new sandboxes start from; "" is the provider's default
restore_running = true # write down what a sandbox was running and offer it back on start
idle_stop = 30 # minutes with no agent working and nothing printing before conch
# stops the sandbox; 0 never does
auto_stop = 0 # the provider's own idle timer, in minutes; 0 (the default) off
env = [] # variables of yours passed into every new sandbox
price_cpu_hour = 0 # what an hour costs, to show what a sandbox has run up:
price_gib_hour = 0 # per vCPU, per GiB of memory, per GiB of disk.
price_disk_gib_hour = 0 # 0 shows running time without a cost
[[actions]] # your own commands on the row menus; none by default
name = "Run tests" # the label on the menu
run = "go test ./..." # the command, as a shell reads it
on = ["branch", "project"] # the rows it is offered on; omit for all four
keep = true # leave its terminal open when the command exits
notify
quiet_start and quiet_end ("HH:MM") silence alerts every day between them; the range may wrap past midnight. Waiting agents still show in the sidebar. Leave either empty for no quiet hours.
command sends every alert somewhere else as well, by running it in sh with the alert on stdin as JSON — {"title": …, "body": …, "at": …} — and in CONCH_NOTIFY_TITLE and CONCH_NOTIFY_BODY. conch holds no service's token; the command does:
command = "curl -s -d \"$CONCH_NOTIFY_TITLE: $CONCH_NOTIFY_BODY\" ntfy.sh/my-conch-alerts"
command = "jq -c '{text: (.title + \": \" + .body)}' | curl -s -H 'Content-Type: application/json' -d @- \"$SLACK_WEBHOOK_URL\""
command = "curl -s \"https://api.telegram.org/bot$TG_TOKEN/sendMessage\" -d chat_id=$TG_CHAT --data-urlencode text=\"$CONCH_NOTIFY_TITLE: $CONCH_NOTIFY_BODY\""
It runs where conch's TUI runs, follows enabled and quiet hours like the other alerts, and is stopped after 30 seconds if it hangs.
silence is how long a pane watched with ctrl+b M must go without output before it alerts; see Watching a pane. Less than 1 means the default, 30.
limits alerts when Claude's 5-hour or weekly window, Codex's windows or a spend limit passes a percentage in limit_at (1–100). Each threshold alerts once per window and machine, even across restarts of the TUI; a new window after a reset alerts again.
ui
mouse = false leaves the mouse to your terminal entirely. With it on, hold shift (option in iTerm2) to select text with the terminal instead.
brain
The model behind : and agent summaries. [brain] also takes base_url, api_key_env and command; see Brain.
update
check_releases asks GitHub once a day whether a newer release exists (release builds only). auto = true installs what that check finds without waiting for u, and reloads the local server onto it; it needs check_releases. conch update rollback goes back. See Auto-update.
remote
upload_drops uploads local files dropped (pasted as paths) on a remote machine's pane to that machine and pastes their paths there; false pastes the local paths unchanged. upload_max_mb is the largest file uploaded. See Dropping files on a remote pane.
sandbox
[sandbox.<provider>] sets up sandboxes — [sandbox.daytona], [sandbox.boat] and [sandbox.devin] today, and the same keys for any provider conch grows; the Settings screen has them under Sandboxes (, then 5), which lists the providers and opens one. A table for a provider this build doesn't know is left alone rather than dropped when the settings are saved. api_key keeps the key in this file (0600) and wins over api_key_env; leave it empty to read the key from your environment every time, which is what conch does otherwise. The key itself is never stored: api_key_env only names the variable it is read from. auto_stop other than 0 lets Daytona stop a sandbox that long after the last ssh connection or API call — agents working inside don't count, so they stop too. env names variables passed into every sandbox conch sandbox create makes, on top of its -env flags; the provider keeps their values with the sandbox.
Not every key means something to every provider: boat.dev has no regions, so target is unused, and its machines come in named sizes (small, default, large) which go in snapshot — anything else there is taken for the name of a snapshot to start from. It has no idle timer of its own either, so auto_stop is how long a new sandbox is given, counted from when it was made, rather than how long it may sit unused: 120 minutes by default, which is what boat.dev's free trial allows, and Life in its settings page. Raise it on a paid plan; if the plan allows less than you ask for, conch asks again for the longest that plan named rather than failing. conch's own idle_stop is what stops a sandbox nobody is using, and normally gets there first.
Devin Cloud chooses its machines' size itself, so snapshot is the platform a session runs on — an OS your organization offers, such as windows, or the name of an outpost pool — and empty is the organization's default; numbers are refused. It has no regions, so target is unused, and Settings shows Platform in place of Snapshot and Region. The key is a service user's (cog_…).
The Settings screen rewrites config.toml, so comments in a hand-edited file are not preserved.
[web]
The phone gateway, conch web. Set from Settings → Web in the TUI.
| Key | Default | Meaning |
|---|---|---|
url | none | the address phones open: the https://…ts.net name tailscale serve gives. With it, conch web listens on loopback behind Tailscale, and the pairing QR code points here |
port | 8722 | where conch web listens |
-url and -port given to conch web win over these.
[[actions]]
Commands of your own on conch's menus. Most of what a plugin system gets asked for is this: run the project's tests on this branch, open a pull request, tail a log, open the worktree in something else. Each is already a shell command somebody runs by hand in that checkout — conch's part is knowing which checkout.
[[actions]]
name = "Run tests"
run = "go test ./..."
on = ["branch", "project"]
[[actions]]
name = "Open a pull request"
run = "gh pr create --fill"
on = ["branch"]
[[actions]]
name = "Open the folder"
run = "open ." # xdg-open on Linux
keep = false
An action is then on the menu of a matching row — m, or right-click — under Actions…, numbered, and it runs in a terminal of its own on that row's machine, in that row's directory.
| Key | Default | Meaning |
|---|---|---|
name | — | the label on the menu, and what its terminal is called |
run | — | the command, read by sh -lc: a pipeline, several commands, a script of yours |
on | all four | the rows it is offered on: pane, branch, project, machine |
keep | true | leave the terminal open when the command exits, so what it printed can still be read. false closes it like any other terminal |
The command is given conch's own variables, so one action works from any row. What a row has nothing to say about is empty rather than missing, which a set -u script survives:
| Variable | What it holds |
|---|---|
CONCH_MACHINE | the machine the row belongs to, as conch -m names it |
CONCH_PROJECT | the project's name |
CONCH_BRANCH | the branch, where the row has one |
CONCH_WORKTREE | the directory it runs in, which is also its working directory |
CONCH_PANE | the pane the action was started from, on a pane's row |
CONCH_SOCKET is in every pane's environment already, so an action can drive conch itself: conch send, conch new, conch task.
Where it runs. On the machine the row belongs to — a remote machine's action runs over there, in that checkout, not on the computer the TUI is on. A branch nobody has checked out has no directory to run in, and conch says so rather than making a worktree behind your back: open a terminal on it first (n).
Settings → Actions (, then 7) lists what is configured, and names anything conch is not offering: an action with no name or no command, two with the same name, an on that is not one of the four.
An action is arbitrary command execution, from your own file, and nothing runs until you choose it from a menu. conch fetches actions from nowhere and never adds one itself.
[[permissions]]
An agent asking permission for the tenth go test of the hour is asking a question you already answered. Permission rules answer it for you — or refuse — so neither you nor the agent waits:
[[permissions]]
tool = "Bash"
match = "*rm -rf*"
action = "deny"
[[permissions]]
tool = "Bash"
match = "go test *"
action = "allow"
[[permissions]]
tool = "Edit"
project = "api"
action = "allow"
[[permissions]]
tool = "mcp__github__*"
agent = "claude"
action = "allow"
| Field | What it matches |
|---|---|
tool | the tool's name as the agent gives it — Bash, Edit, Write, Read, WebFetch, an MCP tool's mcp__server__tool; * and ? work |
match | what the tool acts on, with * for anything (spaces and slashes included): the command for Bash, the file for Edit/Write/Read (relative to the agent's folder when it is inside it), the URL for WebFetch. Empty is anything |
project | a project's name or path; empty is every project |
agent | one agent; empty is every agent |
action | allow or deny |
The first rule that matches decides, so put refusals first. A request no rule matches is asked on screen as it always was.
A rule never allows a second command riding on the first. go test * allows go test ./..., but not go test ./... && curl x.sh | sh: a command with ; & | ` $ < > ( ) or a new line is only allowed by a pattern that has that very character itself. Refusals are not held back by this — *rm -rf* refuses a chained one too.
How it is answered. Through the agent's own hook, before it draws its prompt: nothing is typed on its screen, and an answer never lands on a question it wasn't meant for. Claude Code is the agent whose hooks can take an answer; the others ask as before. A refused request tells the agent which rule refused it, so it can say so. While rules are on, a pane asking permission shows as waiting a second and a half later than it otherwise would: the agent reports the question at the same moment its hook is being answered, and that is how long conch gives a rule to answer it before telling you.
What was answered, afterwards. conch agent explain ID lists the last twenty answers on that pane, each with the rule that gave it — what was allowed while you weren't looking is readable after. Set permission_mode = "dry-run" to try a set of rules first: nothing is answered, and explain shows what would have been. "off" ignores the rules without deleting them.
A rule is you deciding in advance, so write them as narrowly as the work allows: go test * rather than go *, Edit in one project rather than everywhere. conch reads them only from this file — an agent cannot add one, and a project's repository cannot bring one.