An option added to spec.syncPolicy.syncOptions, a sync triggered again, and nothing changes. Or it changes for every resource except one — the one that had the problem in the first place.

Sync options are not a flat list of switches. They live at two levels, they do not all modify the same thing — some change the kubectl command actually executed, others only the scope — and for several of them, the lower level wins.

Two levels, and an explicit precedence rule

The first level is the Application: spec.syncPolicy.syncOptions, a list of strings acting as the default for every resource of that Application.

# Application level: the default for every resource of this Application
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground

The second level is the resource itself, through the argocd.argoproj.io/sync-options annotation. Several options are concatenated into a single value, comma-separated; surrounding whitespace is trimmed.

# Resource level: applies to this object only. Comma-separated values are
# supported in a single annotation.
kind: PersistentVolumeClaim
metadata:
  annotations:
    argocd.argoproj.io/sync-options: Prune=false,Delete=false

The documentation is explicit for Prune and Delete: an option set on the resource always overrides the policy defined at the Application level. That is the first thing to check when a global setting appears to be ignored on one specific object — it is not ignored, it is overridden.

Not every option is available at both levels. CreateNamespace and FailOnSharedResource are set on the Application. Force is set as a resource annotation. Replace, ServerSideApply, PruneLast, Prune and Delete accept both.

The options that change the command being run

By default, Argo CD applies manifests with client-side kubectl apply, which relies on the kubectl.kubernetes.io/last-applied-configuration annotation to store the previous state. Three options replace that command, and that is exactly what makes them risky.

Replace=true switches to kubectl replace or kubectl create. The documented motivation is size: a manifest too large will not fit into the last-applied-configuration annotation, capped at 262,144 bytes. The documentation attaches its own warning to this option — resources may have to be recreated, with the outage that implies.

Force=true, in practice combined with Replace=true, goes through kubectl delete then create. This is the case for Jobs meant to run on every sync.

# Delete + recreate on every sync. Documented as destructive.
metadata:
  annotations:
    argocd.argoproj.io/sync-options: Force=true,Replace=true

ServerSideApply=true runs kubectl apply --server-side --force-conflicts. It answers three distinct needs: exceeding the annotation size limit without the side effects of Replace, patching a resource the Application does not fully manage, and relying on field ownership (managedFields) rather than on a last-applied state. Two details matter. First, the option can be disabled per resource with ServerSideApply=false as an annotation, even when it is enabled on the Application. Second, Replace=true takes precedence over ServerSideApply=true — combining both does not do what one expects.

The partial patch deserves its own example, because it requires a second option. Handing Argo CD a manifest containing a single field is not valid against the Kubernetes schema:

# Partial manifest: valid input for server-side apply, invalid for the
# Deployment schema. Validation must be disabled alongside.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 3
spec:
  syncPolicy:
    syncOptions:
      - ServerSideApply=true
      - Validate=false

Validate=false on its own serves a different case: Kubernetes types using RawExtension, which kubectl refuses to validate.

The options that decide what disappears

Two families coexist here, and confusing them is the classic mistake. Prune governs deletion during a sync, when a resource is no longer described in Git. Delete governs deletion when the Application itself is deleted. A PVC you want to keep in both cases needs both annotations.

Prune=false prevents deletion. The direct and often unwelcome consequence: the Application stays OutOfSync as long as Argo CD believes a resource should be pruned. The sync status panel shows that pruning was skipped, and why.

Prune=confirm and Delete=confirm, since Argo CD 2.14, add a human confirmation for critical resources — a Namespace, typically. Confirmation goes through the UI, the CLI, or by manually applying the argocd.argoproj.io/deletion-approved annotation with an ISO timestamp — on the Application, not on the resource, which is counter-intuitive right after a paragraph where the option itself is set as a resource annotation. Until confirmation arrives, the operation remains in Syncing.

The version floor matters more here than elsewhere: on an earlier release the annotation is accepted and does nothing, silently. A safety option that fails quietly is worse than no option at all.

PrunePropagationPolicy picks between background, foreground and orphan; the default is foreground. PruneLast=true pushes pruning into a final implicit wave, after the other resources are deployed and healthy.

The options that narrow the scope

ApplyOutOfSyncOnly=true changes the default behaviour, which is to apply every object of the Application on each sync. On an Application holding thousands of them, this weighs on the API server and inflates the status.operationState.syncResult.resources field, with a knock-on effect on the datastore behind the API. The difference with a selective sync is documented and decisive: with ApplyOutOfSyncOnly, hooks still run and the sync is recorded in history. A selective sync does not run hooks and is not recorded, which rules out rollback.

FailOnSharedResource=true makes the sync fail when a resource of the Application is already applied in the cluster by another Application. Without it, Argo CD applies silently — the failure mode that shows up once Applications are generated, the same ground as the tenant trap in ApplicationSets.

The option that connects the diff to the sync

RespectIgnoreDifferences=true is the one you look for without knowing it exists. By default, spec.ignoreDifferences is used only to compute the diff, that is, to decide whether the Application is synced. At sync time the desired state is applied as-is, and the patch is computed by a three-way merge between the live state, the desired state and the last-applied-configuration annotation — the merge nobody actually looks at.

# Without RespectIgnoreDifferences, spec.replicas is ignored when computing
# the diff, then written back anyway on the next sync.
spec:
  ignoreDifferences:
    - group: 'apps'
      kind: 'Deployment'
      jsonPointers:
        - /spec/replicas
  syncPolicy:
    syncOptions:
      - RespectIgnoreDifferences=true

With the option on, the desired state is pre-patched before being applied. One documented caveat: it is only effective when the resource already exists in the cluster. On creation there is no live state, and the desired state goes in as-is.

What looks like a sync option but is not

Three neighbouring mechanisms are configured elsewhere, and hunting for them in syncOptions wastes time.

Phases and waves use argocd.argoproj.io/hook and argocd.argoproj.io/sync-wave, separate annotations. Worth noting in passing: during pruning the wave order is reversed, resources in higher waves are pruned first.

argocd.argoproj.io/compare-options: IgnoreExtraneous is yet another annotation, excluding a resource from the application’s overall sync status. It only affects sync status — if the resource’s health degrades, the application degrades anyway.

Finally, spec.syncPolicy.automated and spec.syncPolicy.retry are siblings of syncOptions, not options. They decide when a sync starts, not how it runs — the reconciliation loop itself is covered in declarative deployment with ArgoCD.

So the right question before adding an option is not “which one fixes the symptom”, but “at which level does it sit, and what does it change in the command”. A destructive option set at the Application level applies to resources nobody had in mind when writing it.

Further reading: choosing between ArgoCD and Flux revisits what this option model implies for operators, and the French reading path la chaîne CI/CD puts the topic back into the full pipeline.