03/Documentation
Configure
The .env, your own slash commands, keys, quick replies and typefaces; appearance, Zen mode, language
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 youCollie 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.
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.
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.
Your own slash commands
Put machine-specific commands, such as a Herdr plugin /fork-in-herdr or a custom /deploy, in commands.toml. This is one of four 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 |
launchers.toml | none, matched by exact command instead | none | yes, but an already-open tab re-reads the rows only on its next load |
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"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.
Your own key presets
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.
Your own quick replies
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.
Your own launchers
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 Recent, 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 pack (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.
Your own typefaces
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.
Attachments
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_TYPES adds 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 pack, 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.
Multi-session
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.
Dark mode / light mode
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 terminal mirror is deliberately different
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.
Zen mode
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.
Language
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.