tomo docs
home github

Examples

Recipes for the things you'll actually do, with real transcripts trimmed to the lines that teach something.

Sync a project to a GPU box #

When you want your laptop's edits to land on a beefier build/train box in real time.

From inside the project, initialize it and name the peer once:

$ tomo init
initialized Tomo project in .tomo

$ tomo sync dev@gpu-box:~/proj
recorded remote dev@gpu-box:~/proj
connecting to gpu-box over SSH
pushed remote binary tomo 0.2.0 (x86_64-unknown-linux-musl, 48234112 bytes)
peer connected
synced src/main.rs
synced Cargo.toml

That one command did a lot: it recorded the peer in .tomo/config.toml, opened the SSH connection, copied a matching static binary into the remote's .tomo/bin/ (nothing was pre-installed there), started it, and began syncing. On a terminal it opens a live view of the session — the stream, a status heartbeat, and a conflict center on c. d detaches and leaves it syncing; q stops it (with a confirm). A bare tomo sync later reuses the recorded peer; add --plain for the classic line stream.

Get build artifacts back automatically #

When the box compiles or trains something and you want the output on your laptop without copying it back by hand.

Sync is two-way by default, so anything the remote writes flows back the same way your edits go out. Start a session, edit locally, and when the remote build drops a binary you'll see it arrive:

$ tomo sync dev@gpu-box:~/proj
peer connected
synced src/main.rs            # your edit, going out
synced target/release/proj    # the box's build, coming back

The artifact lands at the same path locally. If you'd rather a path only move one way — say, pull the server's dist/ but never push your local one — that's a direction rule in config (direction = "pull" or "push").

Ignore the stuff you don't want synced #

When target/ or other generated junk shouldn't cross the wire.

node_modules is already ignored by default (along with .git, Python virtualenvs and caches, .terraform, and the rest), so you rarely need a rule at all. For something Tomo doesn't ignore out of the box — like target/, which is left syncable on purpose so build artifacts can flow back — add an ignored rule to .tomo/config.toml (a trailing slash expands to match everything under the directory):

[[rules]]
pattern = "target/"
class = "ignored"

Now only real source crosses over — target/ because of your rule, node_modules for free:

$ tomo sync --local-peer /tmp/peer   # (any sync; local peer shown for brevity)
$ ls /tmp/peer/src         →  main.rs
$ ls /tmp/peer/target      →  No such file or directory   # your ignore rule
$ ls /tmp/peer/node_modules →  No such file or directory   # ignored by default

You don't need rules for the usual suspects: .git, .tomo, node_modules, Python virtualenvs and caches, .terraform, editor swap/backup files, and .DS_Store are ignored by default. To sync a normally-ignored tree anyway it takes two rules (one to un-ignore the directory, one for its contents) — the re-include nuance covers it.

See what changed and roll back #

When you want your history: what changed, when, and how to undo it.

Every save is a version. List a file's history newest-first:

$ tomo log notes.txt
history of notes.txt (newest first):
  #3   present   32 B   replica 10f71c82   local   2s ago   {10f71c82:3}
  #2   present   29 B   replica 10f71c82   local   4s ago   {10f71c82:2}
  #1   present   18 B   replica 10f71c82   local   6s ago   {10f71c82:1}

Drop the path for repo-wide recent activity (tomo log). Compare two versions with diff:

$ tomo diff notes.txt --version 2 --against 3
diff notes.txt: version #2 → version #3 (- base / + target):
  line one
- line two
+ CHANGED two
  line three

Roll back. With no version it undoes the last save; pass --version for an exact one:

$ tomo restore notes.txt
restored notes.txt to version #2 (29 B)

$ tomo restore notes.txt --version 1
restored notes.txt to version #1 (18 B)

The restore is written safely (staging + atomic rename). If a session is running, the restored bytes sync to the peer as an ordinary edit. Use --stdout to inspect an old version without touching your working copy.

Deal with a conflict #

When the same file got edited on both machines and you want to know what happened to the other version.

Say both sides changed config.txt while briefly disconnected. Sync doesn't stop and nothing is lost — both machines independently pick the same winner, and you get a non-blocking heads-up:

$ tomo status
conflicts  1
⚠ 1 unresolved conflict — see `tomo conflicts list`
$ tomo conflicts list
1 unresolved conflict(s) (oldest first):
  #1    [OPEN ] config.txt  4s ago
        winner #2 present 26 B  replica e4a0830e  local
        loser  #3 present 26 B  replica 1b47b618  remote

show gives you the full picture, including a diff of the two heads:

$ tomo conflicts show 1
conflict #1 on config.txt (unresolved)
  recorded 11s ago (2026-07-20 23:40:35Z)
  winner #2 present 26 B  replica e4a0830e  local
  loser  #3 present 26 B  replica 1b47b618  remote

diff (loser → winner, - loser / + winner):
  shared config
- port = 7070
+ port = 9090

Then pick a resolution. Keep what's on disk and just acknowledge it:

$ tomo conflicts resolve 1 --keep-current
acknowledged conflict #1 on config.txt (kept current file)

Or take the version that lost (it was preserved in history all along):

$ tomo conflicts resolve 1 --take-loser
took loser #3 of conflict #1: wrote 26 B to config.txt

Either way sync never paused, and the version you didn't keep is still recoverable. Mass-acknowledge everything with tomo conflicts resolve --all --keep-current.

Same thing, in the TUI. If a session is attached, the badge in the status line already told you a conflict landed. Press c for the conflict center: the winner-vs-loser diff is right there, framed as "on disk now" vs "in history". Enter keeps the current file, t takes the version in history, b keeps both — and the bottom line shows the exact tomo conflicts resolve command it just ran, so you never have to leave the keyboard to learn the CLI. u undoes a verdict you change your mind about.

Detach and attach from another terminal #

When you want the session running in the background and a window onto it from wherever you happen to be.

A sync session is a server; the terminal you started it from is just one client. Start it detached and it returns to your shell, still syncing:

$ tomo sync -d dev@gpu-box:~/proj
session started (pid 48120) — attach: tomo attach · stop: tomo stop

Now open a second terminal — another SSH pane, a tmux split, anywhere on the same machine — and attach. You get the same live view; detaching never disturbs the session, and several terminals can attach at once:

$ tomo attach              # the TUI, from any terminal
$ tomo attach --plain      # the line stream instead
$ tomo logs -f             # or just tail the session log

When you're done, stop it from anywhere (idempotent — a second stop is a friendly no-op):

$ tomo stop
stopped session (pid 48120)

A bare foreground tomo sync is exactly this — sync -d plus an implicit attach — so there's one session, whichever way you start it.

Script the event stream #

When you want CI, a hook, or a notifier to react to what the session is doing.

Everything the TUI shows rides a versioned event feed on the session's local control socket. tomo events --json streams the raw records — pipe them through jq and build whatever you like. Ping yourself the moment a conflict lands:

$ tomo events --json | jq -r 'select(.event=="conflict") | .path'
src/train.py

Or watch what's arriving from the peer without opening a UI:

$ tomo events --json \
    | jq -r 'select(.event=="synced") | "  ↓ \(.path)  \(.size)B"'

The records carry a "v":1 version and are additive-only, so a script written today keeps parsing tomorrow's sessions. It's the same contract as --json on status, log, and conflicts. More on the feed in The interface.

Work offline and reconnect #

When the network drops mid-session and you keep working.

A dropped link isn't fatal. The session keeps watching, indexing, and versioning locally, and reconnects on its own:

$ tomo sync dev@gpu-box:~/proj
peer connected
peer disconnected — queueing changes (peer disconnected)
# …you keep editing; nothing blocks…
reconnected
peer connected

On reconnect, the normal handshake is the offline queue: it reships every change the peer doesn't already have, so the two sides converge to identical trees. If both machines edited the same file while apart, that's just a conflict — handled the same non-blocking way, nothing lost.

Sync through a jump host or nonstandard SSH #

When the box is only reachable via a bastion, on a weird port, or with a specific key.

Tomo reads ~/.ssh/config the way your own ssh does, so if ssh vm1 works, so does Tomo. Given a block like:

Host vm1
    HostName 10.0.0.5
    User deploy
    Port 2222
    IdentityFile ~/.ssh/deploy_ed25519
    ProxyJump bastion

you just use the alias — the jump, port, user, and key all come from the config:

$ tomo sync vm1:~/proj

When it doesn't work, tomo dev ssh-route shows exactly the route Tomo resolved — the analogue of ssh -G — so you can spot a missing key or an unpicked jump:

$ tomo dev ssh-route vm1
route to vm1: vm1 (10.0.0.5 via bastion)
  proxy jump: bastion
  [jump] bastion
      hostname               bastion.example.com
      port                   22
      user                   jump
      identity files         /home/you/.ssh/id_ed25519
  [target] vm1
      hostname               10.0.0.5
      port                   2222
      user                   deploy
      identity files         /home/you/.ssh/deploy_ed25519
      stricthostkeychecking  yes

The full directive matrix (ProxyJump chains, known-hosts semantics, IdentitiesOnly, and more) is in Configuration.

Try it on two local folders #

When you want to see how it behaves without a second machine.

--local-peer syncs against a directory on the same box (it spawns a served peer rooted there). No SSH, no remote — handy for a first look:

$ tomo init
$ tomo sync --local-peer /tmp/peer
local peer at /tmp/peer
peer connected

Edit a file in your project and watch it appear under /tmp/peer; delete one and watch it go. Everything else on this page — history, conflicts, ignore rules — works exactly the same way against a local peer.