Configuration
Everything lives in <project>/.tomo/config.toml. The file is optional; with no file you get the defaults. There's no global config.
Path rules and classes #
Every path in the tree is classified into exactly one of three classes:
| Class | Synced? | Versioned? | For |
|---|---|---|---|
synced+versioned | yes | yes | source files — the default for any path no rule matches |
synced+unversioned | yes | no | build artifacts you want mirrored back without history |
ignored | no | no | things that must never cross the wire (e.g. target/) |
You assign classes with [[rules]] entries — git-style glob patterns, matched against the path relative to the project root:
# History capture: adaptive (default) | every-change | off | { interval_ms = 5000 } [history] mode = "adaptive" [[rules]] pattern = "target/" # trailing slash expands to target/** class = "ignored" [[rules]] pattern = "dist/**" class = "synced+unversioned" direction = "pull" # both (default) | push | pull
Glob semantics #
*and?never cross a/(they match within one path component).**matches across directory boundaries —target/**,**/*.log.- A pattern without a
/is still root-anchored:*.logmatches only top-level logs; use**/*.logto match in every directory. - A trailing slash auto-expands:
target/is treated astarget/**.
Last matching rule wins #
When several rules match a path, the last one in the file wins (git-style precedence). This lets a narrow later rule carve an exception out of a broad earlier one:
[[rules]]
pattern = "build/**"
class = "ignored"
[[rules]]
pattern = "build/keep/**" # re-include one subtree
class = "synced+versioned"pull-only rule on a server's build directory stops a target/-spraying build from growing your history at build speed. Class and direction are enforced on receive as well as send — an ignored or wrong-direction path is refused at ingress, never applied, never recorded — so a peer on an older binary can't sneak one past you.Built-in default ignores #
Unless you turn them off, Tomo prepends a small set of built-in ignored rules for editor/tool churn, OS metadata, database sidecars, git metadata, and large regenerable dependency/cache trees. They are applied before your rules, so (last match wins) any [[rules]] entry for the same glob overrides a default. This is the complete current list:
| Group | Patterns |
|---|---|
| vim/editor temp | **/*.swp, **/*.swx, **/.*.sw?, **/4913 |
| emacs/gedit | **/*~, **/.#*, **/#*# |
| OS metadata | **/.DS_Store, **/Thumbs.db |
| SQLite sidecars | **/*.sqlite-wal, **/*.sqlite-shm, **/*.sqlite-journal |
*.db sidecars | **/*.db-wal, **/*.db-shm, **/*.db-journal |
| git metadata | **/.git, **/.git/** |
| dependency / cache trees | **/node_modules, **/.venv, **/venv, **/__pycache__, **/.pytest_cache, **/.mypy_cache, **/.ruff_cache, **/.terraform (each with a matching /** for its contents) |
| IDE / editor project dirs | **/.idea, **/.vscode, **/.vs, **/.fleet, **/.zed (each with a matching /**), plus **/*.sublime-workspace |
| web-framework dev caches | **/.next, **/.nuxt, **/.svelte-kit, **/.astro, **/.angular, **/.turbo, **/.parcel-cache, **/.expo, **/.vite, **/.docusaurus, **/.cache (each with a matching /**) |
The .git defaults cover the root repo, nested repos, and submodules — .git is a directory in an ordinary clone but a file in a worktree or submodule, so the bare **/.git matches both forms. Syncing .git would cross-contaminate two independent repositories' objects and index; two .git trees must ignore each other entirely. Lookalikes like .gitignore, .gitattributes, and .gitkeep are not matched and sync normally. The .db/.sqlite journal patterns are anchored to those two stems on purpose — a bare *-journal would wrongly swallow an ordinary file like travel-journal.
The dependency and cache trees — node_modules, Python virtualenvs (.venv/venv) and tool caches (__pycache__, .pytest_cache, .mypy_cache, .ruff_cache), and Terraform's .terraform — are each ignored as a directory and its contents, following the same two-pattern shape as .git. They share one rationale: they're large, machine-regenerable, and frequently platform-specific (native node_modules addons, absolute-path virtualenvs, Terraform provider binaries), so dragging them across a Mac↔Linux pair is wasted bytes at best and broken on the peer at worst. Only whole path components match, so a directory merely named like a default — node_modules_backup, my.venv — syncs normally.
IDE and editor project dirs — .idea, .vscode, .vs, .fleet, .zed, and Sublime's per-user *.sublime-workspace — are ignored by default too. They mix shareable settings with machine-local state (indexes, caches, absolute SDK paths) that churns constantly and is plain wrong on the other machine — a synced .idea/ happily points your Linux box at your Mac's JDK. If your team checks .vscode into the repo, git already carries the shared copy; want tomo to sync it anyway? Re-include it with the two-rule pair below. (*.sublime-project, the shareable half of Sublime's pair, stays synced.)
Web-framework dev caches — .next, .nuxt, .svelte-kit, .astro, .angular, .turbo, .parcel-cache, .expo, .vite, .docusaurus, and the generic .cache — are each a dev server's or CLI's regenerable scratch dir: machine-local, disposable, and often platform- or absolute-path-specific, so they never help the peer. They're ignored as a directory and its contents, like the trees above, and every one is overridable. These are dev-server state, not deploy artifacts — the build-output decision below is unchanged. (.cache is dot-prefixed so collisions are unlikely, and its bare glob matches a lone file named .cache as well as the directory, which is intended.)
target/, build/, dist/ — are not default-ignored, because getting a remote build's artifacts to flow back to your laptop (as synced+unversioned, pull-only content) is one of Tomo's flagship use cases; a blanket default would break that out of the box, so you opt out with a single one-line ignored rule if you want to. Nuxt/Nitro's .output is left synced for the same reason (it's the built deploy artifact, unlike the ignored .nuxt dev-state dir), and so is public — it's a build output in Gatsby but the source static-assets dir in most other frameworks, so default-ignoring it would silently drop hand-authored files. And .env is often the very file the remote needs to run the app, so ignoring it by default would silently break deploys.Disable the whole set with:
[sync]
default_ignores = false # ON by defaultRe-including an ignored tree takes two rules #
Because the scan won't descend into an ignored directory, un-ignoring a whole tree needs two rules — one for the directory itself, one for its contents — exactly like git:
[[rules]] pattern = ".git" # un-ignore the directory itself… class = "synced+versioned" [[rules]] pattern = ".git/**" # …and everything under it class = "synced+versioned"
What you cannot reconfigure #
.tomo/** is always ignored, enforced below the rule engine as a constant — no rule, however hostile, can make Tomo watch, sync, or version its own state directory. Only the first path component counts, so a nested a/.tomo in your own tree is an ordinary path.
That directory also holds a generated .tomo/README.md — a short brief for any coding agent working in the tree, explaining that files can change under it in real time (a peer's edits land in milliseconds) and that every save is recoverable with tomo log / tomo restore. Because .tomo/** never syncs, each machine writes its own copy; Tomo regenerates it but never touches your CLAUDE.md/AGENTS.md. Who's on the other end lives in .tomo/state/status.json (or tomo status --json).
History modes #
The [history] section chooses how often a save becomes a version:
| Value | Behavior |
|---|---|
"adaptive" | Default. Flushes ~75 ms after a save under light load so every save is versioned, then escalates the flush interval under storms to coalesce bursts into checkpoints, and decays back to immediate when idle. |
"every-change" | Every canonical change becomes its own version, with a literal 0 ms window. No coalescing. |
"off" | No history capture. (Sync still works; you just keep no versions.) |
{ interval_ms = 5000 } | Coalesce a burst into one version per fixed interval, in milliseconds. |
The remote peer #
tomo connect and the first tomo sync write the [remote] section for you. You can also write it by hand:
[remote] host = "you@gpu-box" # SSH target (resolved through ~/.ssh/config) path = "/home/you/proj" # the peer's project-root path identity = "~/.ssh/gpu_ed25519" # optional explicit key (else agent / config / defaults)
SSH config support #
The target you give is resolved through ~/.ssh/config first, exactly as your own ssh would — so Tomo authenticates and connects wherever ssh host already works, including hosts reachable only through a jump host or with a non-default key. Point Tomo at a different file with TOMO_SSH_CONFIG. Supported directives:
| Directive | Support |
|---|---|
Host | Pattern blocks with * / ? / !, plus the global pre-Host section. First-obtained-wins per ssh_config(5). |
HostName | Alias → real host. Literal only — %h and other token substitution is not performed (rare in practice). |
User, Port | Honored. |
IdentityFile | Accumulates (multiple allowed); tried in order. |
IdentitiesOnly | yes skips ssh-agent keys and uses only the declared identities. |
StrictHostKeyChecking | yes / no / accept-new / ask. ask is treated as yes since Tomo is non-interactive. |
UserKnownHostsFile | One or more paths; default ~/.ssh/known_hosts + known_hosts2. /dev/null means nothing is known. |
GlobalKnownHostsFile | Default /etc/ssh/ssh_known_hosts + …_known_hosts2. Always consulted for lookup, never recorded into. |
ProxyJump | Comma-separated [user@]host[:port] chain; each hop is resolved recursively (cycle guard, depth cap 8). none disables. |
Include | Glob-expanded, relative to ~/.ssh, processed in place. |
Unknown keywords are ignored (their names collected for a debug line).
Authentication order #
Keys are tried in this order, first accepted wins: ssh-agent (unless IdentitiesOnly yes) → the [remote] identity you recorded → the IdentityFiles from ~/.ssh/config → the built-in ~/.ssh/id_ed25519 / id_rsa. Encrypted (passphrase) keys are out of scope for v0.
Known-hosts semantics #
- Lookup spans every user known-hosts file and the global set (OpenSSH parity). Recording (under
accept-new) targets only the first writable user file — never the global set, never/dev/null. StrictHostKeyChecking noaccepts any key unpinned;accept-newrecords an unknown key but rejects a changed one with the usual MITM error;yes/default keeps strict behavior.- Port-form fallback: for a non-default port the
[host]:portform is tried first, then — matching OpenSSH's "found matching key w/out port" compatibility — the plain port-lesshostform of the same files. A port-form match/mismatch always wins; a plain-form match connects and logs a compat note. Recording always uses the port-qualified form. - Before negotiating, Tomo scans those files for the key types already recorded and biases the host-key-algorithm order toward them (as OpenSSH does), so a host recorded only under, say, ECDSA isn't wrongly reported "not found".
Environment variables #
| Variable | Effect |
|---|---|
TOMO_COLOR | always / never / auto — force or auto-detect colored output. (Auto: colored on a terminal, plain when piped.) |
NO_COLOR | The standard opt-out; any value disables color, same as TOMO_COLOR=never. |
TOMO_ASCII | Set to 1 to force ASCII-only glyphs (no Unicode check-marks or badges). |
TOMO_SSH_CONFIG | Path to an SSH config file to use instead of ~/.ssh/config. For test hermeticity and power-user redirection. |
TOMO_INSTALL_DIR | Read by the installer (install.sh): where to place the binary. Defaults to ~/.local/bin. |
--json output is always byte-identical regardless of these — styling never leaks into machine-readable surfaces.
Troubleshooting #
ssh but Tomo can't connect." Run tomo dev ssh-route <target> and diff it against ssh -G <target>. ssh-route is Tomo's ssh -G analogue: it prints, per hop, the resolved hostname/port/user, the identity files it will try, the StrictHostKeyChecking policy, the known-hosts files consulted, and the ProxyJump chain — pure resolution, no network. A mismatch (a missing IdentityFile, an unsupported %h token in HostName, a jump host Tomo didn't pick up) shows up immediately.~ gotcha. tomo sync host:~/proj expands ~ on the server — that is the form to use. The old two-argument form (tomo sync host ~/proj) was removed precisely because an unquoted ~ there lets your local shell expand it first, turning it into an absolute path under your local $HOME before Tomo sees it. A stray second argument now gives a clear error showing the combined host:~/proj target to use instead; and as a backstop, a remote path that is absolute and under the local $HOME is still refused before any SSH with a copy-pasteable fix.