# Sync policies

> Control who wins when local .env and Shelve diverge — push guards, pull merge, and diff.

Sync policies live under `sync` in `shelve.json` (and can be enforced on the server per project). They answer: **should a push from my laptop overwrite production?** **Does pull replace my whole .env or merge?**

## Quick example

```json [shelve.json]
{
  "$schema": "https://shelve.cloud/schema.json",
  "slug": "my-team",
  "project": "my-app",
  "defaultEnv": "development",
  "sync": {
    "protectedEnvironments": ["production", "preview"],
    "default": {
      "onPushConflict": "overwrite",
      "pullMode": "replace"
    },
    "environments": {
      "development": {
        "sourceOfTruth": "local",
        "onPushConflict": "overwrite"
      },
      "production": {
        "sourceOfTruth": "remote",
        "allowPush": false,
        "pullMode": "merge"
      }
    }
  }
}
```

## Policy fields

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Values
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        sourceOfTruth
      </code>
    </td>
    
    <td>
      <code>
        remote
      </code>
      
       | <code>
        local
      </code>
    </td>
    
    <td>
      Hint for <a href="/docs/cli/sync-policies#sync-command">
        <code>
          shelve sync
        </code>
      </a>
      
      : pull when <code>
        remote
      </code>
      
      , push when <code>
        local
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        onPushConflict
      </code>
    </td>
    
    <td>
      <code>
        overwrite
      </code>
      
       | <code>
        skip
      </code>
      
       | <code>
        fail
      </code>
      
       | <code>
        prompt
      </code>
    </td>
    
    <td>
      When a key exists on Shelve with a <strong>
        different
      </strong>
      
       value than local. Default: <code>
        overwrite
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        pullMode
      </code>
    </td>
    
    <td>
      <code>
        replace
      </code>
      
       | <code>
        merge
      </code>
    </td>
    
    <td>
      <code>
        replace
      </code>
      
      : rewrite <code>
        .env
      </code>
      
       (legacy). <code>
        merge
      </code>
      
      : remote keys win; <strong>
        local-only
      </strong>
      
       keys are kept.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        allowPush
      </code>
      
       / <code>
        allowPull
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      Hard block with <code>
        PUSH_BLOCKED
      </code>
      
       / <code>
        PULL_BLOCKED
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        protectedEnvironments
      </code>
    </td>
    
    <td>
      <code>
        string[]
      </code>
    </td>
    
    <td>
      Sets <code>
        allowPush: false
      </code>
      
       for listed env names.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        requireConfirmation
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      Extra confirmation even if <code>
        confirmChanges
      </code>
      
       is false.
    </td>
  </tr>
</tbody>
</table>

Per-environment overrides go in `sync.environments.<name>`. Defaults apply via `sync.default`.

## Server policies

Project **Settings → Sync policy** stores `syncPolicy` on the project. Server rules **cannot be relaxed** from `shelve.json`: if the server sets `allowPush: false`, the CLI cannot override it.

Protected environments reject API writes with `ENV_PROTECTED`.

## Commands

### `shelve diff`

Compare local `envFileName` with Shelve (no writes). Safe for agents with `--json` (no secret values).

```bash [terminal]
shelve diff --env staging
shelve --json diff --env staging
shelve diff --env staging --show-values
```

### `shelve sync` {#sync-command}

Apply the effective policy for the environment:

- `sourceOfTruth: remote` → pull (respects `pullMode`)
- `sourceOfTruth: local` → push (respects `onPushConflict`)

```bash [terminal]
shelve sync --env development
shelve sync --env production --dry-run
shelve sync --yes --env staging
```

`--dry-run` reports the planned action and diff without writing.

## Environment variables

<table>
<thead>
  <tr>
    <th>
      Variable
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        SHELVE_SYNC_ALLOW_PUSH=0
      </code>
    </td>
    
    <td>
      Disables push for all environments in this process
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        SHELVE_SYNC_ALLOW_PULL=0
      </code>
    </td>
    
    <td>
      Disables pull for all environments
    </td>
  </tr>
</tbody>
</table>

## Error codes

<table>
<thead>
  <tr>
    <th>
      Code
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        PUSH_BLOCKED
      </code>
    </td>
    
    <td>
      <code>
        allowPush: false
      </code>
      
       or protected environment
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        PULL_BLOCKED
      </code>
    </td>
    
    <td>
      <code>
        allowPull: false
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        SYNC_CONFLICT
      </code>
    </td>
    
    <td>
      <code>
        onPushConflict: fail
      </code>
      
       (or <code>
        prompt
      </code>
      
       in non-interactive mode)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ENV_PROTECTED
      </code>
    </td>
    
    <td>
      Server rejected a write to a protected environment
    </td>
  </tr>
</tbody>
</table>

See [Troubleshooting](/docs/cli/troubleshooting).

## Monorepos

Put `protectedEnvironments` in the **root** `shelve.json`; package-level files can override `sync.environments` for each app.
