skills/argocd-outofsync-empty-diff-operator-crd/SKILL.md
Diagnose and fix ArgoCD apps that remain OutOfSync even though 'argocd app diff' shows nothing. Use when: (1) an app is OutOfSync but diff output is empty or blank, (2) a Kubernetes operator manages its own CRD (e.g. ClusterPolicy, Kafka, Prometheus, etc.) and sets default field values after the initial Helm install, (3) ignoreDifferences already covers image/version/repository but the operator also defaults other fields not present in the Helm values. Covers the Python direct-comparison technique to identify the exact differing fields and the ignoreDifferences fix. Also applies when 'argocd app diff --refresh' shows nothing but hard-refresh still shows OutOfSync.
npx skillsauth add aldengolab/lorist argocd-outofsync-empty-diff-operator-crdInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
An ArgoCD app reports OutOfSync but argocd app diff shows no output.
The resource causing the drift is a CRD managed by a Kubernetes operator
that sets post-install default field values ArgoCD never rendered.
argocd app get <app> shows OutOfSync on a CRD resourceargocd app diff <app> returns empty output (blank, no lines)argocd app get <app> --hard-refresh still shows OutOfSyncignoreDifferences is already set for some fields (e.g. image/version/repository)argocd app diff filters its display through ignoreDifferences — so it can
appear empty while the sync-status computation still flags the resource as
OutOfSync. The actual differences are in fields the operator sets as defaults
after Helm creates the CRD instance, but which Helm leaves as null/absent.
# Desired state as ArgoCD sees it (what Helm rendered)
argocd app manifests <app-name> | python3 -c "
import sys, yaml, json
content = sys.stdin.read()
for doc in content.split('---'):
try:
d = yaml.safe_load(doc)
if isinstance(d, dict) and d.get('kind') == '<YourCRDKind>' \
and d.get('apiVersion', '').startswith('<api-group>'):
print(json.dumps(d.get('spec', {})))
break
except: pass
" > /tmp/desired_spec.json
# Live state from cluster
kubectl get <crdkind> <name> -o jsonpath='{.spec}' | \
python3 -c "import sys,json; print(json.dumps(json.loads(sys.stdin.read())))" \
> /tmp/live_spec.json
import json
with open('/tmp/desired_spec.json') as f:
desired = json.load(f)
with open('/tmp/live_spec.json') as f:
live = json.load(f)
def diff(d1, d2, path=''):
diffs = []
if not isinstance(d1, dict) or not isinstance(d2, dict):
if d1 != d2:
return [(path, d1, d2)]
return []
for k in set(list(d1.keys()) + list(d2.keys())):
p = f'{path}.{k}'
v1, v2 = d1.get(k), d2.get(k)
if isinstance(v1, dict) or isinstance(v2, dict):
diffs.extend(diff(v1 or {}, v2 or {}, p))
elif v1 != v2:
diffs.append((p, v1, v2))
return diffs
for p, want, got in diff(desired, live):
print(f'{p}\n desired: {want}\n live: {got}')
Fields where desired is None and live has a value are operator-set defaults
that need to be added to ignoreDifferences.
In the ArgoCD Application template, add each diffing field path:
ignoreDifferences:
- group: <api-group>
kind: <CRDKind>
jqPathExpressions:
# existing entries ...
- .spec.someField.operatorDefault
- .spec.anotherField
Commit and push. ArgoCD will pick up the change on next sync.
argocd app get <app-name> --hard-refresh -o json | \
python3 -c "import json,sys; d=json.load(sys.stdin); \
print('sync:', d['status']['sync']['status'])"
# Should show: sync: Synced
argocd app manifests returns an empty desired spec file, the Helm source
may use a multi-source Application — check which source index renders the CRD.managedFieldsManagers in ignoreDifferences is an alternative approach when
the CRD has proper SSA managed fields — but many operator-created CRDs have
zero managed fields entries, making jqPathExpressions the only viable option.development
Build a UEFI Secure Boot PXE netboot server for Ubuntu autoinstall. Use when: designing or implementing network boot infrastructure for automated Ubuntu provisioning with Secure Boot enabled. Covers the complete chain: signed shim+GRUB selection, TFTP layout, kernel parameters, autoinstall config requirements, and post-install bootstrapping scripts. Also applicable when debugging an existing PXE setup that uses the wrong GRUB binary or config paths.
development
Design pattern for running a persistent PXE/TFTP server that safely coexists with already-installed nodes. Use when: building PXE infrastructure that should stay always-on, designing automated bare-metal provisioning in GitOps/Kubernetes environments, or any PXE setup where UEFI boot order has network boot first. Eliminates boot loops without requiring UEFI firmware changes.
development
This skill governs all prose output — Claude's own responses, documentation, PR descriptions, commit messages, README content, comments, and any text the user asks to draft or edit. It should also be used when the user asks to "review my writing", "edit this for clarity", "make this clearer", "simplify this text", "rewrite this", "check my prose", "tighten this up", or "make this more concise". Based on George Orwell's "Politics and the English Language" (1946).
development
Debug Kubernetes pods using hostNetwork: true that crash with "Address already in use" or "failed to create listening socket for port N". Use when: (1) a hostNetwork pod container is in CrashLoopBackOff and logs show a port bind failure, (2) the port works fine in non-hostNetwork pods but fails with hostNetwork, (3) you need to identify which host-level process holds a port from within Kubernetes (no SSH). Covers /proc/net/udp inspection and kubectl debug node with nsenter.