Clone a cloud folder into a real directory on your disk, edit it with your own editor, git checkout and scripts, and sync the two back together.

```mermaid
flowchart LR
  C["Cloud folder /Projects"] -->|clone| D["./projects on disk"]
  D -->|"your editor, git, scripts"| D
  D -->|"sync (both ways)"| C
  C -->|"sync (both ways)"| D
```

## Clone a folder

```bash title="Terminal"
monofile clone /Projects ./projects
cd projects
```

The second argument is optional: `monofile clone /Projects` creates `./Projects`, and cloning `/` (your whole workspace) creates `./monofile`. The target has to be new or empty, and it cannot already be a clone, so two folders cannot merge by accident.

Cloning writes a `.monofile/` state directory and then runs one full pull. From that point on the folder is a **mirror**. `sync` and `status` find it by walking up from your current directory, the way git finds a repository.

## Sync both ways

```bash title="Terminal"
monofile sync              # one round, then exit
monofile sync --dry-run    # show what would happen, touch nothing
monofile status            # what is pending, without syncing
```

One round does everything in both directions: new and changed files travel whichever way they need to, deletes propagate, and renames arrive as renames. `status` is the read-only version and always exits `0`, like `git status`.

To keep it running, leave a watcher on:

```bash title="Terminal"
monofile sync --watch
monofile sync --watch --interval 15
```

The watcher reacts to local file changes immediately and polls the cloud every five seconds by default. Rounds never overlap. Press <kbd>Ctrl</kbd> + <kbd>C</kbd> and it finishes the round in flight, releases the folder lock, and exits `0`. You cannot combine `--watch` with `--dry-run`.

If you only want a one-time copy, use the stateless pair. `monofile pull <remote> [local]` downloads a tree, `monofile push <local> [remote]` uploads one, neither keeps any state, and `push` never deletes anything.

## What lands in the folder

Text files arrive as text files, at the same relative paths they have in the cloud. Images and video arrive as real bytes up to 25 MB. Anything larger becomes a zero-byte **placeholder**, so the folder still shows you the shape of the tree without pulling a gigabyte you did not ask for. Deleting a placeholder locally never deletes the real file in the cloud.

The CLI skips some things and prints the reason for each:

| Skipped | Why |
|---|---|
| Binaries that are not image or video | There is no upload path for them |
| Symlinks | Never followed, never synced |
| Text files over 4 MB | Too large for the filesystem API |
| Names Windows cannot create | Reserved names and characters such as `< > : " \| ? *` |
| Paths that collide only by letter case | On a case-insensitive disk, only the first can exist |

## The `.monofile/` directory

Every mirror carries its own state. You can read all of it; do not hand-edit it.

| File | What it is for |
|---|---|
| `config.json` | Which remote folder this mirrors, which server it came from, the media size cap, and when it was cloned |
| `manifest.json` | The **base**: the agreed state at the end of the last sync, which is what makes a three-way merge possible |
| `media.json` | Content hashes for media files, plus the list of paths currently held as placeholders |
| `ignore` | Paths the engine never looks at |
| `lock` | Present only while a sync or watch is running |

The CLI writes everything here atomically. The layout is shared with the desktop app, so a folder cloned by the CLI opens in the desktop and a folder the desktop created syncs from the CLI.

A mirror remembers which server it came from. Point the CLI at a different origin inside a clone and it stops with a usage error.

## The ignore file

`.monofile/ignore` is gitignore-flavoured and starts with `.git`, `node_modules`, `.monofile` and `.DS_Store`. A bare name matches that name in any directory, a pattern containing `/` matches a path relative to the mirror root, and a trailing `/` matches a whole subtree. `.monofile` itself is always ignored.

```text path=".monofile/ignore"
# .monofile/ignore
.git
node_modules
.DS_Store
build/
*.log
notes/scratch.md
```

## How conflicts are resolved

A conflict is when the same file changed on both sides since the last sync. It is not an error and does not stop the round; the only question is which version wins, and `--on-conflict` answers it.

| Mode | What happens |
|---|---|
| `local` (default) | Your local file wins and is pushed. The version it replaced is still in the file's cloud history |
| `remote` | The cloud version wins and overwrites your local file |
| `copy` | Both survive. The CLI copies your version aside as `notes.conflict-20260917-143005.md` and pushes it as a new file, while `notes.md` takes the cloud version |

> [!NOTE]
> A local win writes a new cloud version, so the overwritten cloud content stays in the file's history and [[Reverting Safely]] can put it back.

> [!TIP]
> `monofile status --on-conflict remote` tells you which way each conflict would go before you commit to it.

## Deletes and renames

Deletes propagate in both directions, and the engine prunes emptied directories on both sides. The base manifest is what lets the engine tell "you deleted this" from "this arrived from the cloud after your last sync", which a simple two-way comparison cannot.

The engine detects renames by content hash, on either side, and sends them as renames. The file keeps its identity, so its version history, its share link and every `[[Wiki-Link]]` pointing at it survive a plain `mv` on your laptop.

## One push, one changeset

A single sync round groups all its cloud writes into one changeset. When a round pushed nine files, that is one entry in `monofile changesets list` and one id to undo:

```bash title="Terminal"
monofile changesets list --limit 5
monofile changesets revert <id>
```

See [[Changesets]] for the full model.

## One syncer per folder

While a sync or a watch is running it holds `.monofile/lock`. A second `sync` on the same folder stops immediately with exit code `6` and tells you which process holds it. The CLI replaces a lock whose process is gone, so a crash does not leave the folder wedged. The desktop app respects the same lock and shows the folder as locked while your CLI watcher owns it.

## Exit codes

| Code | Meaning |
|---|---|
| `0` | Synced. Conflicts resolved by your policy are not failures |
| `1` | At least one operation failed. It is retried on the next round |
| `2` | Usage: not a clone, a bad flag, a non-empty clone target, or an origin mismatch |
| `4` | The remote folder does not exist |
| `5` | The key lacks a scope the push needed. Nothing in that batch was applied |
| `6` | Another sync or watch holds the folder lock |
| `7` | The server could not be reached |

## Next steps

:::cards
- [[CLI Commands]] — Every command, flag, JSON output shape and exit code.
- [[The Desktop App]] — Install, sign in, and work in local folders or a synced cloud workspace.
- [[Changesets]] — Runs grouped into one reviewable, revertable unit.
- [[Filesystem API]] — The manifest and batch endpoints for bulk sync clients.
:::
