# Branching and Writing

Pinning makes the past reproducible; *branching* makes the present safe to change. `tether new -b NAME` starts a bookmark and, with it, a branch named after it in every system, off the pinned state. You write through native handles, and [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) pins the results and moves the bookmark. Nothing you do on a bookmark touches `main` in any system until you [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) it.

This page continues from [Pinning](pinning.md). It adds a Lance feature store and the analysis code itself, so the recipe covers four systems.


# Register a few more objects

``` bash
tether add features --kind lance s3://bucket/features.lance
tether add code     --kind git ../analysis-code --remote origin
tether commit -m "Add feature store and code"
```

`code` pins a *separate* git repository, the analysis code, at its `HEAD` sha (as a tag), so a dataset commit also names the code that produced it. Do not register the repository the dataset lives in: every `tether commit` moves its sha, so every trunk `pull` would find it moved and pin it again.


# A bookmark is a branch in every system: `tether new -b`

`init` left the working copy on the trunk bookmark, `main`. There, every object's working ref is its *upstream* branch, such as `main` in Icechunk and in Neon, and [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) pins wherever those are. To work without touching them, start a bookmark:

``` bash
tether new -b relabel
```

    on bookmark relabel; working refs set up
      code          -> tether.ws.0a1b2c3d.relabel  (created on first writable open)
      db/metrics    -> tether.ws.0a1b2c3d.relabel  (created on first writable open)
      features      -> tether.ws.0a1b2c3d.relabel  (created on first writable open)
      zarr/imaging  -> tether.ws.0a1b2c3d.relabel  (created on first writable open)

`jj log` now shows a `relabel` bookmark where `main` is, and every Forkable system will get a `tether.ws.0a1b2c3d.relabel` branch. The names match, so `jj log` and `icechunk branch list` tell one story. [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) decided the names and the fork points (the pins at `main`) and wrote nothing. Forking is **lazy** by default: the first writable [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) of an object creates its branch. A bookmark that reads three objects and writes one ends up with one branch, not four.

| Object | Pin at `main` | Branch for `relabel` (created on first write) |
|----|----|----|
| `zarr/imaging` | Icechunk tag | Icechunk branch `tether.ws.0a1b2c3d.relabel` |
| `db/metrics` | Neon branch | Neon child branch `tether.ws.0a1b2c3d.relabel` |
| `features` | Lance tag | Lance branch `tether.ws.0a1b2c3d.relabel` |
| `code` | git tag | git branch `tether.ws.0a1b2c3d.relabel` |

`0a1b2c3d` is the dataset's id (from `tether.toml`), so two datasets sharing a store never collide. Objects in one system share its branch. `raw/plate1` has no working ref: files cannot be forked.

Three more forms of [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new): `tether new relabel` from another checkout joins the bookmark (refused while a checkout works on it, unless `--shared`), `tether new main` returns to the trunk, and `tether new REV` for a revision with no bookmark gives a read-only working copy.


## Lazy or eager

`tether new --eager` (or `[new] fork = "eager"` in `tether.toml`) creates every branch during [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new). Every fork is a copy-on-write pointer, so the difference is not the cost of a branch. It is what an idle branch *holds* and *needs*:

|  | Lazy (default) | Eager |
|----|----|----|
| Branches created | only for objects you actually write | one per Forkable object, every [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) |
| What a forgotten workspace leaves behind | nothing for untouched objects | a branch per object, each keeping its snapshot alive; Neon counts them against the branch quota |
| Credentials needed at [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) | read only | write, for every system |
| First writable [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) | pays the fork (one metadata write and a pin check) | just opens |
| `--dry-run` | shows `defer-fork`; the fork happens inside [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open), which has no dry run (`new --eager --dry-run` previews it) | shows every [fork](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.fork) |
| The pin is deleted before you write (`gc` on another machine) | [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) fails with `pin missing` | the branch already exists and keeps the data |
| Pre-creating branches for a job | `Repo.materialize_fork(key)` per object, or `--eager` | automatic |

If one system refuses to fork (quota, network), [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) still records the branches it created and names the object that failed. Run [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) again once the cause is gone; existing branches are reset onto the pin, never duplicated.

**Pin-less objects are not deferred, unless you ask.** With `--pin record` there is no tag, only a snapshot id, version, commit or LSN the system will eventually expire. A branch on it is the only thing that stops that, so [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) forks such objects at once, even in lazy mode.

`new --defer-record` (or `[new] record = "defer"`) defers them like the rest. That suits a bookmark that ends before the store could expire the state, such as a [PR preview](sharing-and-ci.md#a-pr-preview) that writes a few of many stores. Until a writable [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) forks it, nothing of tether's holds such an object's state. [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) warns per object, saying whether its upstream branch still sits at that state or only the store's retention keeps it, and [status](../reference/backends.VerifyReport.md#tether.backends.VerifyReport.status) repeats the warning until the fork. Once the store expires the state, reading the object on that bookmark fails too, and so does the fork (`recorded state is gone`). `new --no-defer-record` forks them at once.

A writable [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) never resets a branch on its own. It reuses one at the pin, or one a sibling object already writes through, and resets one only onto the head [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) reviewed. Otherwise it stops, so [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) can decide with the head in view.


# Write through native handles

[open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) without `-r` returns a **writable** handle for Forkable objects, positioned on the working ref. tether hands you the system's own objects and gets out of the way.


## Icechunk

``` python
import zarr
from tether import Repo
from tether.handles import IcechunkHandle

repo = Repo.find(".")
h = repo.open("zarr/imaging")
assert isinstance(h, IcechunkHandle) and not h.read_only
print(h.branch)                       # tether.ws.0a1b2c3d.relabel

group = zarr.open_group(h.session.store, mode="a")
group["labels"][...] = relabel(group["labels"][...])
h.session.commit("Relabel plate1 with model v3")   # an Icechunk commit on the fork
```


## Lance

``` python
import lance, pyarrow as pa
from tether.handles import LanceHandle

h = repo.open("features")
assert isinstance(h, LanceHandle)
lance.write_dataset(pa.table({"well": ["A1"], "score": [0.91]}), h.dataset, mode="append")
```


## Neon (any Postgres client)

``` python
import psycopg
from tether.handles import NeonHandle

h = repo.open("db/metrics")
assert isinstance(h, NeonHandle)
with psycopg.connect(h.url) as conn:     # URL for the forked branch's endpoint
    conn.execute("INSERT INTO metrics (well, score) VALUES ('A1', 0.91)")
```

Opening a Neon fork writable creates a `read_write` compute endpoint on the branch if none exists. Neon pins are *child branches* of the branch they were taken on, which shapes what [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) and `gc` can do afterwards; see [Caveats: Neon](caveats-and-performance.md#neon).


## Code

``` python
from tether.handles import GitHandle

h = repo.open("code")
assert isinstance(h, GitHandle)
h.sha, h.path        # the fork branch's head; commit your code changes there
```

From the shell, `tether open <key>` prints the same addresses:

``` bash
tether open db/metrics
postgresql://runner@ep-brisk-forest-x9y8z7.us-west-2.aws.neon.tech/metrics?...
tether open features
s3://bucket/features.lance#tether.ws.0a1b2c3d.relabel@v4
```

A handle stays on the branch it was opened on. Every new [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) follows the bookmark the checkout is on *now*, so open again after a `tether new` in another process.


# See what changed

``` bash
tether status --snapshot
```

    dataset 3ba1c07e55d2
      modified  code  [git/forkable]
      modified  db/metrics  [neon/forkable]
      modified  features  [lance/forkable]
         clean  raw/plate1  [file/observed] unrecoverable
      modified  zarr/imaging  [icechunk/forkable]

`main` in every system is untouched: the forks isolate your work.


# Commit: pin the branches, move the bookmark

``` bash
tether commit -m "Relabel plate1 with model v3; scores for A1"
```

      pinned code -> tether.0a1b2c3d.f01a9d3c2b7ee5f6
      pinned db/metrics -> tether.0a1b2c3d.55e2ab7d9c10e5f6
      pinned features -> tether.0a1b2c3d.9e7c4b2f0a61e5f6
      pinned zarr/imaging -> tether.0a1b2c3d.deb4861eb388e5f6
    committed 41c0d9e2b7f3

Each pin points at the state of the bookmark's *branch*: an Icechunk tag on the branch's snapshot, a Lance tag on `(branch, version)`, a Neon branch at the branch's LSN, a git tag on the branch's sha. Then the manifests were committed and `relabel` moved onto the commit, in one jj operation. `main`, the bookmark and every system's branch, has not moved. Compare:

``` bash
tether diff main @- --content
```

        changed  code  [+0 -0 ~1 files]
                   modified  segment.py  +12 -3
        changed  db/metrics
        changed  features  [+1 -0 ~0 fragments]
                      added  fragment 4  +1 rows
        changed  zarr/imaging  [+0 -0 ~1 nodes]
                   modified  /labels  12 chunks

Keep writing on the same branches and committing as often as you like. Each commit pins the latest state and moves the bookmark, and the earlier pins stay valid. [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) is not `jj commit`: it starts nothing new.

To record only some objects, name them, as with `git commit PATH`: `tether commit zarr/ db/metrics -m "..."` pins `db/metrics` and everything under `zarr/`. The other objects keep their last committed manifests, and a later `tether commit` picks them up. Objects that share a branch, such as two databases of one Neon project, are named together ([Selecting objects](cli.md#selecting-objects)).


# Landing it: `tether promote`

Your dataset commit pins the bookmark's state in every system. Landing it means pointing each system's *upstream branch* at that state, then moving the trunk bookmark to your commit. Look at the plan first:

``` bash
tether promote --dry-run
```

    plan: promote (0 write(s))
                    hold  code          [git] main  would fast-forward: main sha=41c0d9e2b7f3… -> sha=f01a9d3c2b7e… (base unchanged since fork); held because db/metrics, features refused, and a bookmark is planned whole or not at all
                  refuse  db/metrics    [neon] main  the neon backend cannot move a branch; Neon cannot promote a child branch into its parent; work on the trunk bookmark ...
                  refuse  features      [lance] main  the lance backend cannot move a branch; Lance cannot move a branch head; write the result onto the base branch ...
                    hold  zarr/imaging  [icechunk] main  would fast-forward: main snapshot_id=K3F9M2A1B7C8… -> snapshot_id=DEB4861EB388… (base unchanged since fork); held because db/metrics, features refused, and a bookmark is planned whole or not at all
                          raw/plate1: not forkable; nothing to promote
                          nothing moved: db/metrics, features refused; land the rest on purpose with `tether promote KEY...`, or write those systems' base branches by hand on the trunk (see the refusals) and promote again

For each Forkable object, tether compares the upstream branch with the **fork point**, the state recorded when the branch was created, and decides:

| Upstream since your fork | Backend has | Outcome |
|----|----|----|
| unchanged | [PROMOTE](../reference/testing.Capability.md#tether.testing.Capability.PROMOTE) | **fast-forward**: upstream moves to the branch's head; nothing is rewritten |
| moved | [MERGE](../reference/testing.Capability.md#tether.testing.Capability.MERGE) | **merge**: the system's own three-way merge (Dolt, git); a conflict is reported and nothing is written |
| moved | [PROMOTE](../reference/testing.Capability.md#tether.testing.Capability.PROMOTE) only | **refuse**, with the system's recipe (Icechunk and Iceberg have no merge) |
| anything | neither | **refuse** (Neon, Lance) |

What lands is what the bookmark's *commit* records, since the trunk moves to that commit. A branch with writes since the last commit is refused: `tether commit` them first.

**A bookmark is planned whole or not at all.** When one system is refused and no keys were named, the others are held and nothing is written. A `main` new in one system and old in another is the half-done state you branched to avoid. Name keys to land a subset on purpose (`tether promote zarr/imaging code`): a refusal then holds nothing else, and the trunk bookmark never moves. A full promotion is refused outright when the trunk bookmark has commits yours lacks, or conflicting targets.

The plan above lands nothing until you decide about `db/metrics` and `features`. Neon cannot promote a child branch, so a database that takes writes on `main` belongs on the [trunk](#working-on-the-trunk). Lance cannot move a branch head yet: write the result onto the base branch from the trunk. [Worked Examples](use-cases.md#reprocess-on-a-branch-then-land-it-or-throw-it-away) shows the two-step landing.

**Applying.** Merges run first, and fast-forwards only once every merge landed, because a fast-forward cannot be taken back. Each base moves only from the head the plan reviewed ([conditional moves](cli.md#conditional-moves)). A backend that fails mid-apply leaves the systems before it landed and the trunk where it was; [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) prints both, exits 3, and a re-run finishes the rest. When a base moved under the plan, the refusal says whether planning again can help.

When everything **fast-forwarded**, the trunk bookmark moves to the commit the plan reviewed. After a **merge**, upstream holds a state no commit has pinned yet: [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) resets your branch onto it (unless the branch gained writes meanwhile) and leaves the trunk. `tether commit` pins the merge, and a second [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) moves the trunk:

``` bash
tether promote                                       # fast-forward what it can, merge where the system can
tether commit -m "Merge plate1 relabel into main"    # only after a merge
tether promote                                       # ...then this moves main
```

`--strategy ff` refuses anything but a fast-forward, which suits CI, and `--strategy merge` refuses plain fast-forwards. `--rev REV` promotes what a *dataset commit* pinned, and never moves the trunk bookmark.


# Throwing it away: `tether drop`

The other ending is one command too: `tether drop NAME`, the opposite of [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote). It is a dry run by default, and the plan lists, in order:

1.  `leave-bookmark`, when this checkout is on it: [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) to `--to` (default: the trunk).
2.  `abandon-commit` for each commit that only this bookmark reaches.
3.  `delete-bookmark`.
4.  The store side: `gc --prune-bookmarks` for this bookmark's branches, the pins nothing references once the commits are gone, and with `--delete-stores` the experimental [created-store step](reclaiming-storage.md#stores-tether-created).

One rule is `drop`'s own: a branch whose head a *dropped commit* pinned is deleted, since that is committed work you throw away by name. A plain `gc` would keep it as "unpinned writes". Writes no commit recorded are kept, and said so, unless `--force-prune`.

`drop` is refused for the trunk, for a bookmark another live checkout works on, and while jj reports a conflicted bookmark or `.tether/` conflict. "Only this bookmark reaches" is exact: another bookmark, a tag, a remote bookmark (`NAME@origin`) or another workspace's working copy keeps the commits. The plan then deletes just the bookmark, and says what still reaches them.

The store actions are a preview, computed as if the commits were gone; apply plans them again after the VCS half, and derives again which commits only the bookmark reaches. The plan goes stale when applied in another checkout, or once the bookmark, this checkout or history moved, or another checkout joined the bookmark. `tether abandon REV --gc` is the surgical form, for commits off a bookmark you keep.

**A drop you regret** is not undoable by tether. Three steps, in order and before the next `gc`:

1.  `jj undo`, or under git `git branch NAME <sha>` from `git reflog`: the commits and the bookmark are back.
2.  `tether repair --all-history` re-pins what those commits recorded, while the stores still have those states.
3.  `tether new NAME` to work on it again, starting from its pins as a fresh fork; the branches `drop` deleted are gone.

A drop interrupted part-way is finished, not undone: `tether undo` and `tether repair --dry-run` name the command that finishes it.


# Positions, and `tether pull`

Between commits each object has a *position*: what the next commit will record. As with files in a VCS, what you did not touch stays as the last commit had it.

- On the **trunk**, a Forkable object's position is its upstream branch head: [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) fingerprints it and pins whatever is there.
- On another **bookmark**, it is the bookmark's branch once that exists. Until the first write it sits at the pin the bookmark started from, and [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) does not contact it.
- Objects whose system has **no branches** (files, Delta, DuckLake) sit at their pin on every bookmark. So does an object registered `--at`, until a trunk `pull` moves it onto the branch.
- An object with **no commit yet** is read once, at the branch head or at `--at`.

`tether pull` is the fetch: it reads the heads of the bookmark's branches and commits what moved onto the bookmark. On the trunk that means every upstream branch and every branch-less object, such as a table's current version or a directory's contents. A nightly `tether pull` on `main` is the dataset trailing the world:

``` bash
tether new main
tether pull
```

      pulled zarr/imaging  snapshot_id=TYZ25DJEHXER -> snapshot_id=AZ1REKMV3HET
      pulled raw/plate1  digest=9f2e1c… -> digest=0b7a4d…
      up to date db/metrics
    committed 8d5719841a09 on main

When nothing moved, nothing is committed. A changed `file = "immutable"` object is refused: accepting it is a re-registration, not a pull. On the trunk, `status --snapshot` shows what a pull would take: [modified](../reference/testing.ObjectDiff.md#tether.testing.ObjectDiff.modified) for moved upstream branches, [behind](../reference/ObjectStatus.md#tether.ObjectStatus.behind) for branch-less objects and `--at` positions.


## Starting from an older snapshot

`--at` sets an object's *initial* position to a state that was never pinned: an older Icechunk snapshot from before you adopted tether, a Lance version, a Dolt commit. `tether log` shows what there is to choose from, with each system's own ids, timestamps and messages:

``` bash
tether log zarr/imaging                         # a registered object
tether log s3://bucket/imaging.icechunk --kind icechunk   # before registering
```

      AZ1REKMV3HETD2X7EW7G  2026-09-05 15:16  Relabel plate1 with model v3  [main]
      TYZ25DJEHXERGBR7H3P0  2026-09-05 14:02  Segment plate1
      M32R7C6M4H4Y8BEGSEK0  2026-09-05 13:40  Convert plate1
      1CECHNKREP0F1RSTCMT0  2026-09-05 13:39  Repository initialized

``` bash
tether add zarr/imaging --kind icechunk s3://bucket/imaging.icechunk --at TYZ25DJEHXERGBR7H3P0
tether commit -m "Adopt imaging at the pre-relabel snapshot"
tether new -b from-pre-relabel
```

`--pick` does the same interactively: it numbers the list and asks which entry to use. The first [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) pins that state, [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) reads it, and a bookmark forks from the pin, as if it had been the branch head. After that `at` has no role: a trunk `tether pull` moves the object onto `main` and drops `at` from the manifest. The per-system forms of `at` are in [Backends](backends.md); Neon and files have none.


## Starting from a branch made outside tether

To start a bookmark from a branch someone made with the store's own tools, or from a tag or a state id, restore onto it on that bookmark:

``` bash
tether new -b try-theirs
tether restore zarr/imaging --at their-experiment
```

The bookmark's branch (`tether.ws.<dataset>.try-theirs`) is forked from the state `their-experiment` names when the plan is made. It is a copy: `their-experiment` itself is never moved or written. The object reads as [modified](../reference/testing.ObjectDiff.md#tether.testing.ObjectDiff.modified) until the next [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) pins it. `--at` takes what `add --at` takes for that backend (Lance: a tag or a version). A branch that holds writes nobody committed needs `--discard`. On the trunk `restore --at` is refused, because there the working ref is the upstream branch itself: register the object `--at` instead, or `tether pull`.


# Working on the trunk

Some objects should never be forked: a production database, or a shared `main` you advance on purpose. That is what the trunk bookmark is for:

``` bash
tether new main
tether open db/prod        # a writable handle to main itself
tether commit -m "Nightly load"
```

On the trunk every Forkable object's working ref is its upstream branch (`locator.branch`, `main` by default; `[vcs] trunk` names the bookmark). [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) returns a writable handle to it, and [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) pins whatever it finds there: reproducibility without branch sprawl. Neon's quiescence check keeps those pins consistent ([commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) refuses while writers are active, unless `--force`). Two checkouts may both work on the trunk, which is what a shared `main` means. Any other bookmark is refused to a second checkout unless `--shared`.


# Stale working copies

Your branch can end up forked from a state that is no longer the dataset's. A teammate committed on the same bookmark (`--shared`), or the bookmark was moved by hand to a revision where an object is pinned differently:

``` bash
tether status
```

    dataset 9d0e5f71ac22 (STALE: zarr/imaging)
      ...

``` bash
tether open zarr/imaging
error: working copy is stale: the committed state of zarr/imaging changed since this workspace forked; run `tether new` to refork before writing
```

The refusal covers the whole workspace, as jj's does: no writable handle and no [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) until [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) decides every fork again. Read-only handles keep working. Your own commits never make you stale, and unrelated objects neither cause nor clear it.

`tether new` decides again from the current pins and clears the flag. What you *committed* is safe, since those pins are tags. Uncommitted writes are not, because re-forking resets a branch onto the new pin. So [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) refuses to reset a branch holding writes beyond what you last committed, and says whose they are:

    $ tether new
    error: refusing to reset working branches with unpinned writes:
      zarr/imaging: tether.ws.0a1b2c3d.relabel has writes since this workspace last committed (snapshot_id=Q7ZK…; this workspace's own); commit them, or pass --discard to throw them away

Commit, or `tether new --discard` if that was scratch work. When the writes are a peer's, on a bookmark you share, `--discard` would erase work that is not yours; `tether new --shared` adopts the branch as it is instead, even while the peer (or a service) keeps writing to it. `tether restore KEY --from REV` resets one object's branch and asks the same question, of the branch as the store has it.


# Undo

Every command that touched a store is in this checkout's operation log (`tether ops`). `tether undo` reverses the newest entry where the store still allows it, deleting what the operation *created*: a commit is uncommitted (the pins stay), a [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) loses the branches it made. It never guesses where something it *reset* should go; that is a `restore` you choose. A created branch that gained writes is kept until you commit them or pass `--discard`. Per command: [the undo table](cli.md#ops-undo-repair).

`jj undo` and `git reset` move the manifests, never the stores: use them for the VCS side and `tether undo` for tether's. tether's log holds deltas, not whole views, so several slips are several `undo`s, newest first.

When the VCS goes around tether (a `jj undo`, `jj abandon` or `git reset` of a dataset commit), [status](../reference/backends.VerifyReport.md#tether.backends.VerifyReport.status) warns per vanished commit, and `ops` flags its entry `(vcs commit gone)`. An *undone* commit leaves its manifests as edits: commit again. An *abandoned* one reverts them: `gc` releases its pins, and `repair` recreates them if it comes back.


# Workspaces and bookmarks

Each jj workspace (or git worktree) is its own tether workspace, with its own untracked `.tether/workspace.toml` saying which bookmark it works on. Two checkouts on two bookmarks never write the same store branch. Two checkouts on *one* bookmark would, so `tether new NAME` refuses a bookmark another live checkout holds unless you pass `--shared`. The trunk is the exception: `main` is shared by nature. Separate clones share no lock and cannot see each other's checkouts ([Two clones, one bookmark](sharing-and-ci.md#two-clones-one-bookmark)).

When the VCS moves a bookmark behind tether's back (`jj bookmark delete`, `rename` or `set`, or a `jj new` that leaves it), `tether status` says so, with what to do. `tether new NAME` returns or continues under the new name, `tether new -b NAME` recreates one, and `gc --prune-bookmarks` releases the branches of a bookmark that is gone.


# Next

[Reclaiming Storage](reclaiming-storage.md): what pins and forks cost, how to preview store writes as plans, and how to release what history no longer names.
