By default, Collie runs in open single-user mode: anyone on your tailnet who can reach the URL has
full control. This triggers the TRUSTED_USER warning. Restrict access:
# in your .env
COLLIE_TRUSTED_USER=you@example.com # your tailnet login — Collie rejects anyone else
COLLIE_PUBLIC_HOSTS=myhost.tail1234.ts.net # only behind your OWN proxy; on a tailnet `collie
# start` discovers this for youEvery Collie setting can be written in a TOML file. Write one with every setting and its default:
collie config initThat writes ~/.collie/config.toml, the machine's own statement, and prints the path. Every key in
it is commented out, so the file as written changes nothing: uncomment a key and restart. Run
collie config init --instance instead to write config.toml beside your .env, which overrides
the machine's file for this one instance.
| file | layer | how it is located |
|---|---|---|
~/.collie/config.toml |
machine | your home directory, or the path in COLLIE_CONFIG |
<config-dir>/config.toml |
instance | beside the .env the CLI already resolves |
The instance file wins key by key, never file by file: a key absent from it keeps the machine file's
value. There is no config.<instance>.toml, because on a Herdr-managed install the config dir is
already per instance, and on a binary install it is ~/.config/collie. A second Collie on one host
therefore puts its own settings in its own config dir, see
docs/deployment.md → Several Collies on one host. A
.collie/ inside a project directory is not read at all.
Three verbs drive the file:
collie config initwrites a commented file with every setting and its default.collie config checkvalidates the two files that would be read, or one file you name.collie config showprints both paths and every setting with its value and where it came from.
The precedence runs default, then ~/.collie/config.toml, then <config-dir>/config.toml, then the
process environment, which includes your .env. collie config show names the winner for every key
at once, so "why is my poll interval still 1500" is answered in one line.
A broken key never stops the bridge. An unknown key or a wrong type is a problem that
collie config check prints and collie doctor reports, and that key alone falls back to what the
environment says. Every other key in the file still applies.
Collie reads config.toml only during startup. Run collie restart after modifying it.
Note. A file holding
[push] vapid_privateor[stt] stt_keyis held to mode 600, exactly as your.envis. If Collie cannot tighten it, those keys alone are dropped andcollie doctorsays so.
Collie loads configuration from a .env file in ~/.config/collie. If Herdr manages the
installation, the CLI queries Herdr for the plugin config directory (typically
~/.config/herdr/plugins/config/herdr.collie). Both paths resolve consistently across CLI commands,
so the service reads the file seeded here:
mkdir -p ~/.config/collie && cp .env.example ~/.config/collie/.env
# on a Herdr-managed install, seed Herdr's plugin config dir instead:
cp .env.example "$(herdr plugin config-dir herdr.collie)/.env"Paths below use ~/.config/collie/…. On a Herdr-managed install, replace that prefix with
$(herdr plugin config-dir herdr.collie).
Collie reads .env only during startup. Run collie restart after modifying it.
The .env.example file lists all options, and so does
collie config init --print.
It includes COLLIE_PORT, COLLIE_SERVE_MODE=http (for Headscale or .internal domains), and
COLLIE_SERVE_PORT (to expose HTTPS on a port other than :443; see
docs/deployment.md → Several Collies on one host). The
CLI reads the serve parameters to configure tailscale serve, rather than passing them to the
bridge.
The config file groups every setting under a [section]. The environment name of a key is
COLLIE_ plus the key in capitals, so [bridge] poll_ms is COLLIE_POLL_MS.
| section | what it holds |
|---|---|
bridge |
poll cadence, how many lines are read, where state lives |
network |
the port, the bind address, allowed hosts and origins |
mux |
which multiplexer this collie mirrors, and where it lives |
access |
the Tailscale identity gate, the device header, the audit trail |
push |
the three Web Push (VAPID) values |
uploads |
the attachment size cap and the extra text types accepted |
journal |
where each harness keeps its own session log |
crew |
the budgets a lead gives a member, and a peer's own browser |
standby |
the deputy's second door: port, bind address, arming |
update |
where releases come from, how many versions stay |
serve |
whether Collie publishes the front door, and on what |
stt |
speech-to-text, absent until collie stt setup runs |
To read history from multiple agent home directories, provide a comma-separated list in
COLLIE_TRANSCRIPT_ROOT.
docs/deployment.md covers custom domains and reverse proxies. Collie enforces a
same-origin policy, so any custom hostname or external TLS terminator must be explicitly
allowlisted:
COLLIE_ALLOWED_ORIGINS=https://collie.example.comWithout this setting, the UI will load as an empty page. See Troubleshooting for details.
Put machine-specific commands, such as a Herdr plugin /fork-in-herdr or a custom /deploy, in
commands.toml. This is one of six config files that share the same reader and load pattern:
| file | scope | confirm/danger flag | live reload |
|---|---|---|---|
commands.toml |
optional, per row | confirm = true |
yes, no restart needed |
keys.toml |
optional, per row | danger = true |
yes, no restart needed |
quick-replies.toml |
optional, per row | none | yes, no restart needed |
theme.toml |
none, the faces are a device setting | none | yes, on the next page reload |
launchers.toml |
none, matched by exact command instead | none | yes, but an already-open tab re-reads the rows only on its next load |
cache-rules.toml |
none, matched by exact rule id instead | none | yes, no restart needed |
These files are unchanged by the config file. They share the instance config.toml's
directory, they keep their own formats and their own live reload, and nothing merges them into it.
Any row with the flag set requires a two-tap confirmation before it fires. Edits to any of these
files take effect without restarting the service. If Collie rejects a row, journalctl --user -u collie -n 20 prints the line number and the error.
cp commands.toml.example ~/.config/collie/commands.toml[[commands]]
scope = "omp" # optional; omit for every pane
command = "/fork-in-herdr"
description = "Fork this conversation into a new herdr tab"
[[commands]]
scope = "claude"
command = "/statusline"
description = "Set the status line"
bar = true
bar_label = "Status"A pane that matches your configured rows displays only those rows. The narrowest row wins, as documented in ADR 0018.
To verify, open a pane and tap /; your rows appear on the first screen.
bar = true also puts the row on the actions row, the one row of buttons above the keyboard. A bar
row is still an ordinary palette row, so it appears on both surfaces. bar_label is the button's
text and defaults to the command name without its slash.
The actions row is a belt, one full-bleed band above the keyboard. Collie's own controls sit directly on the band: Keys, Type, Quick, Agent and the display gear. The running harness's own commands sit beside them, in a section tinted with the harness's brand colour, so you can see at a glance which buttons type into the agent. A sideways drag scrolls the belt; nothing is dropped.
Each harness ships its own buttons, in this order:
| Harness | Buttons |
|---|---|
| Claude Code | Model, Effort, Compact, Resume |
| Codex | Model, Compact, Resume |
| pi | Model, Compact, Tree, Resume |
| omp | Model, Compact, Tree, Resume |
Codex and pi have no Effort button. Codex's /model picker sets the model and the reasoning effort
on one screen, so one button already reaches both. pi has no effort or thinking command at all;
that dial lives inside /settings, a modal the keys pad cannot usefully drive.
omp's Tree button sits between Compact and Resume. It opens omp's own session tree in the mirror, a
picker that jumps to any earlier point in the session. The same command, /tree, is also in omp's
command palette.
Every harness button sends its command bare. Model sends /model, and the agent's own model
picker takes over in the pane. Collie keeps no list of model names, because that list is the
harness's and it changes without telling us. Effort works the same way.
Your bar rows replace the shipped harness segment for the panes they address, and leave it alone everywhere else. The Agent palette is a separate surface and one bar row never blanks it (ADR 0043).
A bar_label longer than 12 characters is shortened and the button still appears. A bar that is
not true or false drops that one row, the same way a bad confirm does.
The row sends while the agent is busy, the same as the command palette. The checkmark appears only when the pane took the text.
A Switch button sits at the belt's right end and opens the pane switcher. It draws the layers mark alone, behind a hairline, and carries no word. A drag up, anywhere on the belt, opens the same switcher. A sideways drag scrolls the belt instead.
To verify, open a pane running Claude Code, Codex, pi or omp; the tinted segment sits at the right of the row above the keyboard. Turn that segment off per device in Settings → Harness shortcuts; Collie's own controls stay.
You can replace the Keys tray's Presets row in keys.toml, located next to commands.toml:
cp keys.toml.example ~/.config/collie/keys.toml[[keys]]
scope = "claude" # optional; omit for every pane
label = "Yes"
keys = ["Down", "Enter"] # several chords go out as one batchWhen a pane matches your defined rows, it displays only your presets instead of the default Ctrl C/D/U/R/L/Z buttons (ADR 0018). The rest of the tray (Esc, arrow keys, Enter/Tab/Space, modifiers, digits, F1–F12) is fixed.
Chords use herdr's syntax, not tmux's:
| key | chord | supported? |
|---|---|---|
| Ctrl+C | ctrl+c (not C-c) |
yes |
| Shift+Tab | shift+tab |
yes |
| Ctrl+F7 | ctrl+F7 |
yes |
| Page Up | — | no |
| Home | — | no |
| End | — | no |
| Delete | — | no |
To verify, open a pane and tap Keys → Presets to view the new buttons. If Collie rejects a row,
check journalctl --user -u collie -n 20 for the error details.
You can customize the Quick dock phrases in quick-replies.toml:
cp quick-replies.toml.example ~/.config/collie/quick-replies.toml[[replies]]
scope = "claude" # optional; omit for every pane
title = "confirm"
items = ["yes", "no"] # sent verbatim, one per buttonWhen a pane matches your rules, your groups replace the default ones
(ADR 0018). The default phrases are
English (yes, commit and push).
Use this file to run in other languages, or to send words like approve to specific harnesses.
Setting scope = "shell" targets standard shell panes, which otherwise only receive y/n.
To verify, open a pane and tap Quick to see your groups. If a row fails to load,
journalctl --user -u collie -n 20 prints the error.
One tap runs a command you declared, in launchers.toml next to keys.toml:
cp launchers.toml.example ~/.config/collie/launchers.toml[[launchers]]
command = "htop" # required; the shell line, typed verbatim into the fresh shell
label = "Top" # optional; defaults to the first word of command
# cwd = "~/dev/collie" # optional; absent means "here" — see belowWhere the tap opens depends on where you tap it, not on the row. From the dashboard, a tap creates a new Space named after the row. From a pane — the switcher sheet you reach by swiping up — a tap opens a new tab in that pane's own Space, beside it.
Either way the bridge types the command into the fresh shell and sends Enter. The command owns
its own lifetime: one that closes itself takes the Space or tab with it, and htop stays until you
quit it.
cwd is where that new Space or tab opens. Pin one (as htop does above) and it wins wherever you
tap the row.
Leave it out and it means "here": the dashboard opens it in your home dir, a pane opens it in that pane's own cwd — one cwd-less row follows you around your checkouts instead of always landing at the top of one.
This file is the allowlist. POST /api/launch accepts only a command that matches a row here
exactly, so a phone can start nothing that is not in the file. Changes apply immediately without a
restart, but an already-open tab re-reads the rows only on its next load.
Your rows appear in two places: a Launch section on the dashboard, which folds like Spaces, and a Launch section in the switcher sheet (swipe up from a pane). A pinned row shows its folder, shortened under home; a cwd-less row says "here" in the switcher (the dashboard already implies home, so it says nothing there). Declare no rows and neither section appears.
On a crew (several machines, one phone-facing lead), each machine reads its own copy of this file — a row launches on whichever machine's dashboard or pane you tapped it from, not on the lead.
To verify, reload the dashboard and look under the herd. If a row fails to load,
journalctl --user -u collie -n 20 prints the error.
The interface font is a per-device setting. Under Settings → Typeface, you can choose between
System, Space Grotesk (the default), and Aldrich. You can add custom fonts in theme.toml, the
fourth configuration file:
cp theme.toml.example ~/.config/collie/theme.toml
mkdir -p ~/.config/collie/fonts
cp departure.woff2 ~/.config/collie/fonts/[[font]]
family = "Departure Mono" # the picker's label AND the CSS family
file = "departure.woff2" # a bare name inside fonts/, woff2 only
weight = "400 700" # optionalCustom fonts append to the built-in list rather than replacing it
(ADR 0033), unlike the behavior in
commands.toml and the other configuration files. Because fonts do not trigger actions, there is
nothing to shadow.
They appear below the three default entries, and each client device selects its own.
Three behaviors to note:
- Layout shift on first load. Custom fonts lack metric-matched fallbacks, which causes a minor layout shift during initial load. Built-in fonts avoid this because their fallbacks are generated at build time.
- Cold-load delay. A cold load fetches the file with a brief delay; a cached client paints immediately.
- Chrome only. The selected font applies only to Collie's chrome. The terminal mirror, transcript, and rendered markdown retain their own typography.
- Live on next reload. Changes do not require a restart, taking effect on the next page reload.
Invalid configurations log errors visible via
journalctl --user -u collie -n 20.
Use cache-rules.toml to change a prompt-cache lifetime that your provider changed.
cp cache-rules.toml.example ~/.config/collie/cache-rules.tomlCollie ships one rule per harness and provider. Collie read each rule from a vendor page on a recorded date. A vendor can change that value without notice. A gateway, a proxy that sits between your agent and the vendor, can change it too. Use this file to override values until Collie ships an update.
[[rule]]
id = "claude.api"
ttl_seconds = 3600
source_url = "https://platform.claude.com/docs/en/build-with-claude/prompt-caching"
retrieved = "2026-09-12"
note = "our gateway sends ttl 1h on every request"Collie keeps a row only when all four rules hold. id must name a shipped rule. ttl_seconds must
be an integer from 1 to 86400. source_url must be a non-empty string. retrieved must parse as a
valid YYYY-MM-DD date, and it must not be later than today. Invalid rows are dropped and logged.
The rest of the file still applies.
Two more rules decide what a file means. Two rows with the same id are not an error: the later row
wins. A note you write as an empty string drops the whole row, so leave the field out instead.
A row binds at the tier rule, the one its id names. The per-model split under that tier moves with
it, so one row covers every model on that tier.
Note. You may change a number. You may not remove its source page or retrieval date. The rule catalog requires both fields.
collie doctor checks three items. cache-claims warns when nobody has re-checked a shipped rule in
180 days. cache-rules names every dropped row and the reason it was dropped. cache-env flags when
you set ENABLE_PROMPT_CACHING_1H or FORCE_PROMPT_CACHING_5M in your shell. The bridge runs as a
separate service and cannot read your agent's environment. Mirror these Claude Code variables here so
the bridge can read them.
These are the rule ids you may write.
| rule id | what it covers |
|---|---|
claude.subscription |
Claude Code on a Claude subscription, Pro or Max |
claude.api |
Claude Code on an API key or a third-party provider |
codex.subscription |
Codex CLI signed in with a ChatGPT plan |
codex.api |
Codex CLI on an OpenAI API key |
pi.anthropic, opencode.anthropic |
pi or opencode talking to Anthropic |
pi.openai, opencode.openai |
pi or opencode talking to OpenAI |
pi.google, opencode.google |
pi or opencode talking to Google |
pi.unknown, opencode.unknown |
pi or opencode on an upstream that documents no TTL |
A chip on each agent pane shows how long that agent's prompt cache stays warm.
Every supported harness caches the current conversation. Harnesses charge lower rates while that cache stays warm. The chip counts down idle time since the agent's last request. It does not track total session age. Each new request resets the timer.
The chip's hourglass carries the state in one of three colours, and the number beside it stays grey.
The hourglass is green while the window is wide, red during the final quarter of it, and blue once
the window expires or a turn pays the full rate. The chip says cold in the blue state, and <1m
instead of seconds when under one minute remains.
The chip sits at the right end of the pane header's second line, after the pane's workspace. On a crew the machine's name sits beside it, on the same line. Tap the chip to view the underlying rule: the rule id, the vendor source page, and the retrieval date.
Note. Values are vendor claims with recorded dates, not live measurements, unless marked measured. Claude Code writes cache windows directly to its transcript, so Claude panes are usually measured.
Some panes show no chip. This is expected. A pane shows a chip only after its agent completes one turn. Collie makes no assumptions before measurement. A pane shows nothing if its harness lacks a journal adapter, or if its vendor publishes no lifetime data.
Collie can also send push alerts to a phone before a cache expires. One variable controls the timing.
| var | default | meaning |
|---|---|---|
COLLIE_CACHE_WARN_SECONDS |
300 |
how many seconds before a watched pane's cache expires the push goes out; floor 30, ceiling 3600 |
This setting controls only the push alert. It does not control when the chip turns red. The red threshold is fixed at one-quarter of each rule's TTL. The push itself fires at whichever is shorter, this setting or half the pane's own cache lifetime, so a five-minute cache (Codex, OpenCode, pi, omp) warns about two and a half minutes before it goes cold, even though that is less than the configured 300 seconds. To enable warnings for specific panes or all panes, see voice-and-push.md.
The paperclip beside the message box uploads a file to the host and puts its path in your message.
# in your .env
COLLIE_MAX_UPLOAD_MB=25 # default 10, floor 1, ceiling 512
COLLIE_UPLOAD_EXTRA_TYPES=rb,ex,zig # bare extensions, no dotCollie saves the file under <state-dir>/uploads with owner-only permissions and appends its
absolute path to your draft. The agent reads it from that path, because a terminal cannot take a
pasted file. Uploads are swept 48 hours after they are written.
| Setting | Default | What it does |
|---|---|---|
COLLIE_MAX_UPLOAD_MB |
10 |
Largest file accepted, in whole megabytes. Out of range or not a whole number falls back to the default and logs a warning. |
COLLIE_UPLOAD_EXTRA_TYPES |
(empty) | Extra text types to accept, beyond the list below. Comma-separated bare extensions; a leading dot is forgiven and anything that is not letters and digits is dropped with a warning. |
Two kinds of file are accepted, and they are checked differently.
Images are identified by their signature bytes, never by their name or their declared type:
png, jpg, gif and webp. SVG is refused on purpose, because it is script-bearing markup
rather than a picture.
Text is identified by its extension, with the bytes as a veto: a file whose first 4 KB contain a
NUL or a stray control byte is refused whatever it is called. The shipped list is md, markdown,
txt, json, jsonl, yaml, yml, toml, csv, tsv, log, xml, html, htm, css,
js, jsx, mjs, cjs, ts, tsx, py, go, rs, sh, bash, sql, diff and patch.
Note.
COLLIE_UPLOAD_EXTRA_TYPESadds text types only. An image needs a signature to check it against, so there is no binary format you can add this way.
Raising COLLIE_MAX_UPLOAD_MB raises two other numbers with it. The bridge reads a whole upload into
memory before it can measure it, so a large cap plus several uploads at once is that much memory. And
the runtime's body limit applies to every route, not only the upload one, so a large cap lets a large
body reach any handler, where that handler's own limit then refuses it. Nothing is deleted before its
48 hours are up, so the uploads directory holds at most what was sent in two days. Raise the number
because you need it, not by default.
In a crew, both settings are per machine, and the machine that stores the file is the one that enforces them. The lead refuses an oversize body before forwarding it, to save your uplink, but it refuses it against its own number. Set the same values on every member, or a peer will refuse what its lead let through.
By default, one Collie instance serves every Herdr session it finds.
COLLIE_MULTI_SESSION=on (the default) discovers and serves every named Herdr session under your
config root, switchable from the header. Setting COLLIE_MULTI_SESSION=off serves only the primary
session. Every discovered session is accessible through the same URL, including private or sandbox
sessions. Security lists this behavior as a sharp edge.
Note. Collie follows your phone's appearance by default.
To pin it, open Settings → Appearance and pick System, Light or Dark. The setting is stored per device in the browser rather than on the bridge. Your phone can remain on Dark while a laptop tracks the OS. The preference persists across reloads and PWA reinstalls on the same device.
The mirror always renders on a dark ground. Light mode inverts the entire element instead of re-colouring individual spans.
Agents emit absolute 24-bit colour codes (38;2;r;g;b) tuned for dark backgrounds, which downstream
parsers cannot reliably remap. Rendered directly onto white, most agent output drops below a 3:1
contrast ratio. Inversion preserves the intended contrast. The measurements are documented in
ADR 0002.
This implementation has two practical consequences:
- Keep your agents configured for dark themes. This is the default for Claude Code, codex, opencode and pi. If an agent uses a light theme, it emits dark-on-light values that become illegible in Collie under both modes. This stems from the agent output rather than Collie itself.
- Diffs and highlighted rows render as dark blocks in light mode. Contrast remains intact, but the visual weight is reversed.
Note. Installed on iOS, in light mode, the status-bar text remains white and can blend into the background. iOS does not allow web apps to update this value dynamically. Run Collie directly in the browser instead of as an installed PWA to avoid this limitation.
Note. Zen mode is off by default.
Enable it in Settings → Zen mode (stored per device in the browser). This adds a Zen mode option to the pane menu, under the ⋮ beside Find and History. Tapping it hides all Collie UI elements: the header, tab and pane strips, agent statusline, and composer docks. Only the terminal mirror remains visible. A floating button in the top-right corner or the Escape key restores the interface.
Zen mode is transient. The configuration persists, but the active state resets when you switch panes or reload the page. Panes always open with standard chrome.
The terminal mirror continues polling in Zen mode, and interactive buffer elements remain functional. Prompt buttons, "Load older", and "Show entire history" controls stay available because they are part of the content stream rather than chrome.
Collie's interface is available in six languages. Configure this under Settings → Language.
- English
- Deutsch
- Español
- 한국어
- 日本語
- 中文
The selection is saved locally in the browser per device. The terminal mirror remains untranslated: it displays the raw output from the agent, while quick replies, menu labels, and key caps match the underlying screen or keyboard names.