# hlix push, pull & sync

**These three commands reconcile a bound folder with its cloud revision head.** `push` sends local changes as a new immutable revision, `pull` brings the cloud head down, and `sync` picks whichever direction is unambiguous and refuses the one that is not.

`pull --force` is the only command in this CLI that can destroy work which was never uploaded. Two gates sit in front of it.

## Prerequisites

- A folder bound by [`hlix import`](/reference/cli/import/) — otherwise every command here exits `not_initialized`
- A credential that reaches the bound workspace

## Usage

```bash
hlix push [--dry-run] [--json]
hlix pull [--force] [--yes] [--dry-run] [--json]
hlix sync [--dry-run] [--json]
```

None of the three accepts a positional argument. The bound project is found by walking **up** from `--cwd`, so running from `packages/api` acts on the project, not a fragment of it.

### Flags

- **`--dry-run`** (`boolean`, all three, default `false`): print the divergence and the verdict the real command would reach, and change nothing. Exits `1` when the verdict is `conflict`, `0` otherwise — including when the real run would stop to ask.
- **`--force`** (`boolean`, `pull` only, default `false`): replace local files with the cloud revision. States "replace my changes". Without local changes it replaces nothing and does not prompt.
- **`--yes`** (`boolean`, `pull` only, default `false`): states "do not ask". Separate from `--force` deliberately — the flag that deletes local files used to be the same flag that skipped every question.
- **`--json`** (`boolean`, all three, default `false`): the dry-run payload is `{ dryRun: true, action, approvalRequired, ...divergence }`; a real run returns its action and the resulting revision identity.

Expected result from a dry run:

```text
sync --dry-run: Would push the local changes as a new cloud revision.

local   generation 3 (rev_01J8Z4) — modified
cloud   generation 3 (rev_01J8Z4)
files   412 local, 410 cloud — 3 local-only, 1 cloud-only, 2 differing
```

## The verdict table

Both sides are compared against the folder's last synced base revision. `local changed` means the manifest hash moved; `cloud changed` means the head's revision ID or generation moved.

| local changed | cloud changed | `push` | `pull` | `sync` |
| --- | --- | --- | --- | --- |
| no | no | `none` | `none` | `none` |
| yes | no | `push` | `none` | `push` |
| no | yes | `conflict` | `pull` | `pull` |
| yes | yes | `conflict` | `conflict`, or `pull` with `--force` | `conflict` |

A dry run computes this with the same function the real command uses, so it cannot promise an outcome the command would refuse.

**`conflict` is the only verdict that exits `1`.** Every other dry run exits `0`, including one that reports it would have to ask first.

### `approvalRequired`

A forced pull over local changes adds one line to the human output, verbatim:

```text
pull --dry-run: Would replace local files with the cloud revision.
Requires --yes (or interactive approval) to execute.
```

and one field to the envelope's `data`:

```json
{"dryRun":true,"action":"pull","approvalRequired":true}
```

That run exits `0`. Needing an answer is not a refusal — a caller who has one executes exactly this plan.
**Approval is an input the dry run does not have:** `action` and `approvalRequired` are separate fields because the same argv ends in a pull for a caller who can answer and in `approval_required` for one who cannot. Branch on `approvalRequired`, not on `action` alone. Reporting a plain "would pull" for both is how this once told a `--json` caller it was about to succeed at something it then refused to do.

### Conflicts and validation

- A scan blocker aborts all three with `scan_blocked` before any comparison — a folder that cannot be scanned cannot be reconciled.
- `push` uses a compare-and-swap against the expected revision ID and generation. A cloud head that moved exits `conflict`: ``Cloud changed since the local base. Run `hlix pull` or resolve with `hlix sync`.``
- `push` with an unchanged manifest is a no-op returning `action: "none"` — it does not manufacture an empty revision.
- An ordinary `pull` refuses over local changes: ``Local files changed since the last sync. Push them, or use `hlix pull --force` to replace them.``
- `pull --force` over local changes asks first, listing the cloud generation, how many local-only files will be deleted, and how many will be replaced. A `--json` or non-interactive caller cannot answer and exits `approval_required` pointing at `--dry-run`; a declined prompt exits `cancelled`.
- After a pull, the local baseline is recomputed from the **checked-out** tree rather than the source manifest — the bundle checkout is a synthetic snapshot commit whose tracked flags can differ.
- `sync` never reaches the forced-pull gate: it only pulls when the working tree is unchanged.

## If reconciliation fails

- `not_initialized` — ``This folder is not bound to an imported Hlix project. Run `hlix import .` first.``
- `conflict` on `sync` — ``Both local and cloud changed since the last sync. Pull or push explicitly after reviewing the conflict.`` Run `hlix pull --dry-run` to see the file counts on each side first.
- `conflict` on `push` — the cloud moved. Pull or sync, then push again.
- `approval_required` on `pull --force` — review with `hlix pull --dry-run`, then repeat with `--yes`.
- `cancelled` — the forced-pull prompt was declined. Nothing was replaced.
- `scan_blocked` — the same blockers `import` reports; see [If the scan blocks the import](/cli/import/#if-the-scan-blocks-the-import).
- `Revision upload has no bundle target.` — the API accepted the revision but returned no upload URL. Retry; the idempotency key makes that safe.

## Next steps

[Push, pull & sync](/cli/sync/)
  [hlix status](/reference/cli/status/)
  [Secrets & protected files](/security/secrets/)
  [CLI reference](/reference/cli/)