This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
GitOps source-of-truth for a 4-node homelab Kubernetes cluster (3x Talos Linux + 1x Linux Mint). Argo CD watches main and reconciles cluster state. There is no build/test/lint pipeline — changes are YAML, validated by Argo CD on sync.
See README.md for the node table, component list, ingress URLs, secret/SOPS workflow, and bootstrap procedure. Don't duplicate that here; read it when you need it.
- Edit a manifest or a Helm
values.yamlunderapps/,cluster/, orinfrastructure/. - Commit and push to the remote — Argo CD reads from GitHub, not the local working tree.
- The local
.git/hooks/post-commithook hits the Argo CD API to hard-refreshapp-of-apps, which cascades to all child Applications. Without the push, the refresh sees stale state. - Verify in the Argo CD UI (
http://argocd.k8s.home) or withkubectl --kubeconfig=./kubeconfig get applications -n argocd.
The repo root contains kubeconfig and talosconfig (gitignored). Use them with --kubeconfig=./kubeconfig and --talosconfig=./talosconfig rather than relying on ~/.kube/config, so commands match what's in this repo.
Wrong-cluster guard (agents and operators): this repo targets the homelab cluster only. Before kubectl / Helm / anything that uses the default kubeconfig, confirm you are on that cluster (e.g. kubectl --kubeconfig=./kubeconfig config current-context, cluster-info, get nodes — expect the node set and endpoints described in README.md, not some other environment). Prefer --kubeconfig=./kubeconfig and --talosconfig=./talosconfig from this repo root. If the active context or API is clearly a different cluster, stop and switch; never apply or debug this GitOps tree against the wrong cluster.
On a new machine, only talosconfig needs to be transferred securely (1Password, scp, AirDrop). Then ./scripts/regenerate-kubeconfig.sh derives a working kubeconfig from it. Avoids transferring two secrets when one is sufficient.
Look in argocd/:
argocd/apps/*.yaml— single-sourceApplications pointing at a directory underapps/orcluster/. Kustomize-rendered. Adding/removing files changes what gets applied.argocd/infrastructure/*.yaml— multi-sourceApplications. One source is the upstream Helm chart repo (e.g.https://prometheus-community.github.io/helm-charts), the other is this repo withref: values, andhelm.valueFilesreferences$values/infrastructure/<name>/values.yaml. To change behavior, edit the values file — do not vendor the chart.argocd/infrastructure/monitoring.yamlis the exception that proves the rule: it declares bothmonitoring(the Helm chart) andmonitoring-extras(a plain Kustomize directory atinfrastructure/monitoring/that excludesvalues.yaml). When adding sibling resources to a Helm-managed component (e.g. an Ingress for Grafana), follow this pattern instead of forking the chart.
selfHeal: truecluster-wide — manualkubectl editis reverted within seconds. To experiment, disable selfHeal on the Application first or your changes will vanish.prune: falseon every workload/infrastructure Application — deleting a file orresources:entry will not remove the live object. Either delete it manually withkubectl delete, or temporarily setprune: true.prune: trueonly onapp-of-apps— removing anApplicationYAML fromargocd/apps/orargocd/infrastructure/will delete the Application (but its workloads survive due to the per-appprune: false).
.sops.yamlencryptsdata/stringDataof any file matching*secret*.yaml. Naming is load-bearing: a secret file not matching the regex will be committed in plaintext.- The age private key lives in
key.txtat repo root (gitignored, mode 600). The same key is mounted into the Argo CD repo-server as a Secret namedsops-agein theargocdnamespace, created during bootstrap. - Each directory containing encrypted secrets ships a
*-secret-generator.yaml(KSOPS generator referencing the secret file by relative path) and lists it undergenerators:(notresources:) inkustomization.yaml. Adding a new encrypted secret requires both the generator file and the kustomization entry, otherwise it never reaches the cluster. - KSOPS path gotcha: in
secret-generator.yaml, thefiles:paths are resolved relative to the kustomization root (wherekustomization.yamllives), not the generator file's directory. So a generator atapps/torrenting/bootstrap/secret-generator.yamlreferenced fromapps/torrenting/kustomization.yamlmust usefiles: - ./bootstrap/secret.yaml, not./secret.yaml.
# Edit an encrypted secret in place
SOPS_AGE_KEY_FILE=./key.txt sops apps/openclaw/secret.yaml
# Encrypt a brand-new secret file (filename must match *secret*.yaml)
SOPS_AGE_KEY_FILE=./key.txt sops -e -i path/to/new-secret.yamlnfs-client(default) — NFS at 192.168.8.246. Use for config volumes and shared data. Avoid for write-heavy workloads (Prometheus TSDB/WAL, Postgres) — latency and locking semantics will bite.longhorn— distributed block storage, only scheduled on Talos workers labeledextensions.talos.dev/iscsi-tools: v0.2.0(currentlytalos-gcx-zwd,talos-worker-pve191-01,talos-gpu-01). Workloads using Longhorn must carry a matchingnodeSelector. Use for databases, Prometheus, anything stateful and write-heavy.local-storage— node-pinned hostPath. Do not use on Talos nodes — the immutable rootfs means arbitrary host paths may not exist after a reboot and the mount fails. OK oncarbon-node(Linux Mint) only.- PVCs are immutable. Changing
storageClassNamein a Helm value or StatefulSet will not migrate an existing PVC. Procedure: scale workload to 0 →kubectl delete pvc(mind thepvc-protectionfinalizer — it clears once no pod references the PVC) → re-sync → operator recreates on the new class. Back data up first.
- Cluster default is
baseline:latestenforcement (set in Talos admission config). Onlykube-systemis exempt out-of-the-box. - Anything that needs
hostNetwork,hostPID,hostPath,hostPort, or privileged containers (notablynode-exporter,qbittorrent's VPN sidecar, the Postgres pod oncarbon-node) needs its namespace labeledpod-security.kubernetes.io/enforce: privileged. - Persist that label via
spec.syncPolicy.managedNamespaceMetadata.labelson the Argo CD Application (seeargocd/infrastructure/monitoring.yaml). Chicken-and-egg warning: if the very pod whose admission is failing is what's blocking the sync,managedNamespaceMetadatamay never apply. Label the namespace by hand first, then let Argo take over.
# Fast-refresh a single Application (the post-commit hook only refreshes app-of-apps)
kubectl --kubeconfig=./kubeconfig annotate app <name> -n argocd \
argocd.argoproj.io/refresh=hard --overwrite
# See what an Application is waiting on when sync is stuck
kubectl --kubeconfig=./kubeconfig get app <name> -n argocd \
-o jsonpath='{.status.operationState.message}'
# Bootstrap a fresh cluster (after key.txt is in place)
./scripts/bootstrap-argocd.sh
# Snapshot all torrenting-stack Postgres DBs to NFS, with 7-day retention
./scripts/backup-postgres.shWhen sync blocks on one unhealthy resource, every other change in that Application waits behind it. Fix or delete the blocker before expecting unrelated edits to land.
- New plain-manifest app: create
apps/<name>/with manifests and akustomization.yaml; createargocd/apps/<name>.yaml(copytorrenting.yamlas a template); commit & push. - New Helm-based infra component: create
infrastructure/<name>/values.yaml; createargocd/infrastructure/<name>.yamlusing the multi-source pattern withref: valuesand$values/infrastructure/<name>/values.yaml; commit & push. - New encrypted secret: name the file
*secret*.yaml, encrypt withsops -e -i, add a sibling*-secret-generator.yaml(KSOPS), and list the generator undergenerators:in the directory'skustomization.yamlwith the path relative to the kustomization root.
talos/ holds machine configs for reference. They are not Argo-managed — changes apply via talosctl --talosconfig=./talosconfig apply-config. Be deliberate: a bad config can brick a node until you reinstall.
terraform/proxmox/ declares the VMs that host the Talos cluster (and other workloads) using the bpg/proxmox provider with two aliased provider blocks (one per Proxmox host — they're not clustered). State lives in-cluster as Secret/terraform-state/tfstate-default-proxmox via the kubernetes backend; locking uses a Lease in the same namespace, so any operator with ./kubeconfig can run plan/apply without clobbering. The Proxmox API tokens are stored alongside, as a SOPS-encrypted Secret managed by apps/terraform-state/ — load them into your shell with source scripts/tf-env.fish. See terraform/proxmox/README.md for the full flow. Apply changes with terraform plan / terraform apply from that directory; this is separate from Argo CD's GitOps loop — Terraform doesn't run automatically on commit (yet).