# Push, pull & sync

Every imported project records a local base in `.hlix/state.json` and a cloud head with an immutable revision ID and monotonically increasing generation. Synchronization compares both sides to that common base.

## Prerequisites

- Run commands from the imported project root
- `.hlix/config.json` contains a project ID
- `.hlix/state.json` contains the last synchronized revision
- Finish and test valuable local work before any forced pull

## Choose the command

Both sides are compared against the base revision they last agreed on: if only one moved, the direction is unambiguous; if both moved, hlix refuses rather than merging.

| Command | Use when | Behavior |
| --- | --- | --- |
| `hlix push` | Local files changed; cloud did not | uploads a new immutable revision using compare-and-swap |
| `hlix pull` | Cloud changed; local did not | verifies and applies the cloud head locally |
| `hlix sync` | You want hlix to choose only when one side changed | pushes, pulls, or reports already synchronized |

`hlix sync` is intentionally not a merge engine. If local and cloud both changed from the same base, it stops with a conflict.

## Look before you act

All three commands accept `--dry-run`. It prints the divergence, takes no action, and reports the verdict the real command would reach.

```bash
hlix push --dry-run
```

Expected result:

```text
push --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 first line is the verdict; the block below is the evidence for it. The verdict is one of four per command:

| Command | Verdict line |
| --- | --- |
| `push` | `Would push the local changes as a new cloud revision.` · `Nothing to push — local matches the last synced revision.` · `Would refuse: the cloud changed since this folder's base revision.` |
| `pull` | `Would replace local files with the cloud revision.` · `Nothing to pull — the cloud head is the local base revision.` · `Would refuse: local files changed. Re-run with --force to replace them.` |
| `sync` | `Would push …` · `Would pull the cloud revision into this folder.` · `Already in sync.` · `Would refuse: local and cloud both changed since the last sync.` |

A dry run that would end in a conflict **exits 1**, the same way `hlix import --dry-run` exits 1 for a blocker. The report is the deliverable, and the exit code still carries the verdict — so a script can gate on it without parsing the text. **`conflict` is the only verdict that exits 1**; every other dry run exits `0`.

### When the real run would stop to ask

A forced pull over local changes is the one case where the verdict alone is not the whole answer. The dry run adds a second line:

```bash
hlix pull --force --dry-run
```

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

local  generation 3 (rev_01J8Z4) — modified
cloud  generation 4 (rev_01J9A2) — ahead
files  412 local, 415 cloud — 2 local-only, 5 cloud-only, 3 differing
```

That run **exits `0`**: needing an answer is not a refusal, and a caller who has one — a terminal, or `--yes` — executes exactly this plan.

In `--json`, the same fact is a field rather than a line:

```json
{"dryRun":true,"action":"pull","approvalRequired":true,"localChanged":true,"cloudChanged":true}
```
**Approval is an input the dry run does not have:** `action` and `approvalRequired` are reported **separately** because the same argv ends differently depending on who runs it: a caller who can answer gets a pull, a `--json` or non-interactive caller gets `approval_required`. 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.

## Push local changes

```bash
hlix push
```

Expected result:

```text
push complete.
```

The new revision becomes head only when the cloud still matches the revision and generation in local state. Protected files are uploaded separately from the Git bundle.
**Tasks and revision head:** Cloud agent work can advance the same revision head. A push is refused while project tasks are active, and a stale push cannot overwrite a revision created by a completed cloud task.

## Pull cloud changes

```bash
hlix pull
```

Expected result: `pull complete.` The CLI verifies bundle size, SHA-256, Git bundle validity, commit SHA, manifest metadata, and every protected file before applying data.

If local managed files changed, pull refuses. After reviewing and backing up those changes, replace them explicitly:

```bash
hlix pull --force
```

### The forced-pull approval gate

`pull --force` is the one command that deletes local work, so it asks before replacing anything. The two flags say different things:

- **`--force`** states *"replace my changes."* It selects the destructive behavior.
- **`--yes`** states *"do not ask."* It answers the prompt in advance.

Interactively, `--force` alone prints what it is about to destroy and waits:

```text
hlix pull --force will replace this folder with cloud revision 4.
Local-only files to delete: 3
Files to replace with the cloud copy: 2
Local changes that were never pushed cannot be recovered by Hlix.
```

Declining exits `cancelled`. A `--json` or non-interactive caller cannot answer a prompt, so it is refused rather than defaulted to yes:

```text
`pull --force` replaces local files. Review with `hlix pull --dry-run`, then repeat with `--yes`.
```

That refusal is `approval_required`. `--force` with no local changes replaces nothing and does not prompt.
**Unpushed work is not recoverable:** hlix stores revisions, not your working tree. A file that was never pushed exists only on your disk, and a forced pull overwrites it with no copy kept anywhere. Run `hlix pull --dry-run` first — the `local-only` and `differing` counts are exactly what is at stake.

For a Git project, pull updates the managed working tree to the cloud commit. For a non-Git import, it replaces files described by the revision manifest. Ignored local-only files are outside that managed set.

## Reconcile automatically

```bash
hlix sync
```

The decision table is exact:

| Local since base | Cloud since base | Result |
| --- | --- | --- |
| no change | no change | `Already in sync.` |
| changed | no change | push |
| no change | changed | pull |
| changed | changed | conflict; no side is overwritten |

## Verify synchronization

```bash
hlix sync --json
```

Expected result: a versioned JSON envelope containing `action`, `revisionId`, and `generation`. Run the command again; it should report an action of `none`.

## If synchronization fails

- `not_initialized`: run `hlix import .` from the project root.
- `workspace_mismatch`: a `--workspace`, `--base-url`, or `HLIX_*` value contradicts this folder's binding. Drop the override, or point `--cwd` at a folder bound to the workspace you meant. Run [`hlix status`](/reference/cli/) to see which source decided what.
- `approval_required`: a destructive step needs an approval this caller cannot give. Review with `--dry-run`, then repeat with `--yes`.
- `cancelled`: you declined the prompt. Nothing was changed.
- ``Cloud changed since the local base. Run `hlix pull` or resolve with `hlix sync`.`` — review before choosing a side.
- ``Local files changed since the last sync. Push them, or use `hlix pull --force` to replace them.`` — push first if the local work matters.
- `Both local and cloud changed since the last sync. Pull or push explicitly after reviewing the conflict.`: preserve local work in a branch or backup, inspect the cloud state, then choose a side explicitly.
- `Project has active tasks`: wait for them to reach a terminal state, pull their resulting head when needed, then retry.
- `Downloaded revision … failed integrity verification`: stop. Do not bypass the check; retry over a trusted network and contact support if it repeats.
- `SECRETS_ENCRYPTION_KEY is not set`: the deployment cannot decrypt protected state. An operator must repair backend configuration.
- `Refusing to replace non-regular protected file`: replace the symlink/device with the intended regular file only after confirming the path.

## Next steps

[Import skills, agents & MCP servers](/cli/resources/)
  [CLI commands](/reference/cli/)
  [Secrets & protected files](/security/secrets/)
  [Troubleshooting](/reference/troubleshooting/)