# Repository Guidelines for AI Agents This document provides essential context and instructions for AI agents working on this homelab Kubernetes cluster repository. ## General Engineering Directives Deliver exactly what was requested. Avoid speculative extras, but include the minimum tests, documentation, and safeguards needed to keep behavior correct and prevent regressions. ### Directive Priority If directives conflict, prioritize: 1. Correctness and safety at external boundaries 2. Explicit user instructions 3. Minimal scope and simplicity ### 1. Think Before Coding Objective: surface ambiguity and tradeoffs before writing any code. - State assumptions explicitly. - If uncertainty would materially change the implementation, ask. Otherwise, state your assumption and proceed. - If multiple interpretations exist, present them — don't pick silently. - If a simpler approach exists, say so. Push back when warranted. ### 2. Simplicity First Objective: write the minimum change that meets the request. - No features, abstractions, or configurability beyond what was asked. - No "flexibility" or "configurability" that wasn't requested. - Prefer the golden path for internal logic; let tests define edge-case expectations. - Add explicit validation and error handling at external boundaries (I/O, network, persistence, auth, parsing, external APIs). - If you write 200 lines and it could be 50, rewrite it. - Apply YAGNI ruthlessly. ### 3. Surgical Changes Objective: every changed line traces directly to the request. When editing existing code: - Touch only what the request requires. Don't "improve" adjacent code, comments, or formatting. - Match existing style, even if you'd do it differently. Don't refactor existing code unless it is part of the request. - Remove imports/variables/functions that YOUR changes made unused. - Don't remove pre-existing dead code unless asked — mention it instead. ### Defaults - Use descriptive, consistent naming conventions. - Write docstrings or comments for public contracts and non-obvious behavior. - Comments and docstrings describe what exists now (or the rationale for the current design), never what the code used to be. No "previously…", "no longer…", "changed from…", or "renamed from…" — that history belongs in commit messages and changelogs. When editing, delete stale historical asides you encounter rather than preserving them. - Use type annotations where the language supports them. - Use structured logging where the project uses logging. - Run lint/format/test through project tooling when available; do not hand-format code. - Write tests for public behavior and regressions, not implementation details. ### Sandbox Limitations - The sandbox may not be able to run installs, network calls, or other privileged commands (permission errors) — attempt the command first rather than assuming failure. - **Delegate to the user** only if a command actually fails on a permission or network error. Describe the exact command to run. - Attempting a command first does **not** relax the [Security Rules](#security-rules): never run a mutating cluster, Flux, or node command without explicit human approval, even to test whether it is permitted. ## Project Overview This is a GitOps-managed homelab Kubernetes cluster using: - **k3s**: Lightweight Kubernetes distribution - **Flux CD**: GitOps continuous delivery tool - **Ansible**: Node preparation and cluster installation - **SOPS + Age**: Secrets encryption The repository is the single source of truth. Changes committed here are automatically applied to the cluster by Flux. ## Repository Structure ```text . ├── ansible/ # Ansible playbooks and inventory for node/cluster management │ ├── inventory/ # Host definitions and variables (includes SOPS-encrypted secrets) │ ├── playbooks/ # Playbooks for cluster operations │ └── roles/ # Ansible roles (including xanmanning.k3s) ├── kubernetes/ # Kubernetes manifests and Flux configuration │ ├── apps/ # Applications organized by namespace │ │ └── // # Each app has its own directory with README.md │ ├── bootstrap/ # Initial cluster secrets (SOPS-encrypted) │ ├── flux/ # Flux configuration and app definitions │ └── templates/ # Reusable templates (e.g., volsync) ├── scripts/ # Utility scripts (bash with shellcheck, python with uv/ruff) ├── docs/ # Human-readable documentation └── .justfile # Root just runner; per-domain `mod.just` files are colocated with their dirs ``` > **Just module layout:** the root `.justfile` declares modules that point at colocated > `mod.just` files (there is **no** `.just/` directory). Modules and their files: > > | Module | File | Domain | > | --------- | ------------------------------------------- | --------------------------------------- | > | `ansible` | `ansible/mod.just` | Ansible operations | > | `kube` | `kubernetes/mod.just` | Cluster ops, local-deploy debug | > | `flux` | `kubernetes/flux/mod.just` | Flux bootstrap/reconcile/suspend/resume | > | `volsync` | `kubernetes/apps/volsync-system/mod.just` | VolSync restic snapshots | > | `db` | `kubernetes/apps/database/mod.just` | CNPG hibernate/resume | > | `rook` | `kubernetes/apps/rook-ceph/mod.just` | Rook-Ceph operations | > | `mlflow` | `kubernetes/apps/datasci/mlflow/mod.just` | MLflow trace management | > | `secrets` | `kubernetes/apps/external-secrets/mod.just` | ExternalSecret maintenance | > | `oauth` | `kubernetes/apps/security/mod.just` | Authelia OAuth client setup | > > Repository and `sops` recipes live directly in the root `.justfile`. ## Nix Development Environment This repository uses Nix flakes to provide a consistent development environment with all required tools (kubectl, flux, helm, sops, etc.). The environment is loaded automatically via **direnv** (`.envrc` → `use flake`, cached by nix-direnv). ### Running Commands (direnv — preferred) Use `direnv exec .` to run any command with the devshell loaded: ```bash direnv exec . ``` This sources the cached devshell (`.direnv/flake-profile-*.rc`) and exports `KUBECONFIG`, `SOPS_AGE_KEY_FILE`, etc. for you — so you do **not** need to set `KUBECONFIG` manually. With a warm cache it does **not** contact the privileged nix daemon, so it works inside a restricted command sandbox. Examples: ```bash # kubectl commands (KUBECONFIG is provided by direnv) direnv exec . kubectl get pods -n network # flux commands direnv exec . flux get hr -A ``` **Cache freshness**: `direnv exec` is daemon-free only while the cache is warm. After changing `flake.nix`/`flake.lock`, a human must re-warm the cache from a normal terminal (with nix daemon access) via `direnv reload` (or `cd` out and back into the repo). An agent in a sandbox that blocks the nix daemon socket cannot rebuild the cache itself — `direnv exec` will fail loudly until it is re-warmed. ### Fallback: `nix develop` If direnv is unavailable, fall back to the `nix develop` wrapper. This talks to the nix daemon socket and will **not** work in a sandbox that blocks it: ```bash nix develop -c env 'KUBECONFIG=./kubeconfig' ``` If running with `nix develop` fails, add `--extra-experimental-features` flags: ```bash nix --extra-experimental-features 'nix-command flakes' develop -c env 'KUBECONFIG=./kubeconfig' ``` ## Command Execution Patterns **IMPORTANT**: Use `just` as the repository command runner. Instead: 1. **Reference the colocated `mod.just` modules** (see the table above) to understand what commands should be run 2. **Execute the underlying commands** using `direnv exec .` with `kubectl`, `flux`, `ansible`, `sops`, etc. Example: - ❌ Don't run: raw mutating commands without understanding module intent first - ✅ Do: Look at `kubernetes/flux/mod.just` (the `flux reconcile` recipe) to see the actual command, then run: ```bash direnv exec . \ flux reconcile kustomization flux-cluster -n flux-system --with-source ``` This keeps command usage consistent and discoverable via `just`. ## Kubectl and Flux Operations - **Use direnv**: All kubectl/flux commands should run via `direnv exec .` (which provides `KUBECONFIG`): ```bash direnv exec . ``` For additional usage directives and security constraints, see [.codex/rules/kubectl.rules](.codex/rules/kubectl.rules). ### Security Rules **CRITICAL**: Agents must never access secret contents. Listing secret names is allowed, but retrieving secret data is forbidden: - ✅ `kubectl get secret` or `kubectl get secrets` (without output flags) - lists secret names - ❌ `kubectl get secret/secrets` with any output flags (`-o`, `--output`, `-ojson`, `-oyaml`, etc.) - ❌ `kubectl get externalsecret/clustersecretstore/secretstore` with output flags - ❌ Any template or jsonpath queries on secret resources **CRITICAL**: Agents must not change cluster state without explicit human approval. "Change cluster state" means any action that creates, updates, deletes, scales, restarts, or otherwise mutates Kubernetes or node resources (including triggering a reconciliation that could apply changes). Examples that require explicit approval (non-exhaustive): - `kubectl apply`, `kubectl delete`, `kubectl edit`, `kubectl patch` - `kubectl rollout restart`, `kubectl scale`, `kubectl cordon/drain/uncordon` - `helm install/upgrade/uninstall` - `flux reconcile`, `flux suspend`, `flux resume` - `ansible-playbook` (anything that changes nodes/cluster) If a request appears to require a mutating action, stop and ask the user to confirm the exact command(s) to run. **Allowed kubectl operations** (read-only, informational): - ✅ `kubectl get` (without output flags) - ✅ `kubectl describe` - ✅ `kubectl logs` - ✅ `kubectl top` - ✅ `kubectl events` - ✅ `kubectl api-resources`, `kubectl api-versions` - ✅ `kubectl cluster-info`, `kubectl version`, `kubectl explain` **Allowed flux operations**: - ✅ `flux get` (sources, kustomizations, helmreleases) - ✅ `flux logs`, `flux check`, `flux tree`, `flux trace` ### Command Syntax - Informational queries (get, describe, logs, events, etc.) are permitted; avoid destructive actions unless explicitly requested. - Command ordering follows the prefix-based ruleset: ` [output flags] [other flags] -n [name]`. Keep the resource type immediately after the verb and put namespace flags last so output-format flags (`-o/--output`) stay adjacent to the type for secret-safety controls. - Examples with ordering: ```bash direnv exec . kubectl get pods -n direnv exec . kubectl get pod -n direnv exec . kubectl logs -n direnv exec . flux get hr -n ``` - Keep commands scoped and explicit; do not rely on default namespaces or contexts when fetching cluster state. ## Networking Architecture Understanding the networking stack is critical for configuring ingress correctly. ### Network Components #### Cilium (CNI) - Provides pod networking and network policies - Replaces kube-proxy with eBPF-based routing - Handles service mesh features #### kube-vip - Provides high-availability VIP for the control plane - Manages the cluster API endpoint #### k8s-gateway - Provides internal DNS resolution for cluster services - Resolves `*.domain.com` queries from internal network - Requires split DNS configuration on home DNS server (e.g., Pi-hole) #### Envoy Gateway (Internal) - Ingress class: `internal` - For services that should only be accessible within the home network - Examples: Hubble UI, internal dashboards, home automation - Does NOT route through Cloudflare #### Envoy Gateway (External) - Ingress class: `external` - For services that need to be accessible from the internet - Routes through Cloudflare Tunnel for security - Requires external-dns annotation for DNS record creation #### Cloudflare Tunnel - Secure tunnel from Cloudflare edge to the cluster - No exposed ports on home network - Provides WAF and DDoS protection #### external-dns - Automatically manages DNS records in Cloudflare - Reads ingress annotations to create/update DNS entries - Watches for `external` ingress class resources #### cert-manager - Provides TLS certificates via Let's Encrypt - Supports both staging and production environments - Uses Cloudflare DNS challenge for wildcard certificates ### Choosing the Right Ingress Pattern For internal-only services: ```yaml apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: my-internal-app spec: parentRefs: - name: internal namespace: network sectionName: https ``` For publicly accessible services: ```yaml apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: my-public-app annotations: external-dns.alpha.kubernetes.io/target: external. spec: parentRefs: - name: external namespace: network sectionName: https ``` ## Application Development Workflow ### BEFORE Modifying Any Existing App 1. **Read the app's README.md**: `kubernetes/apps///README.md` 2. **Read the app's upstream documentation**: Helm chart docs, official docs 3. **Understand dependencies**: Check what other apps this depends on 4. **Review current configuration**: Look at HelmRelease values and customizations ### WHEN Adding a New App Required structure: ```text kubernetes/apps/// ├── README.md # MANDATORY - Document the app ├── ks.yaml # Flux Kustomization for this app └── app/ ├── kustomization.yaml ├── helmrelease.yaml # Or raw manifests └── *.sops.yaml # Encrypted secrets if needed ``` README.md must include: - Purpose and description of the app - Configuration details and important settings - Dependencies (other apps, infrastructure requirements) - Known issues or gotchas - Links to upstream documentation #### Steps 1. Research the application and read its official documentation 2. Create the directory structure 3. Write the README.md first 4. Create HelmRelease or manifests following existing patterns 5. Add to `kubernetes/flux/apps.yaml` if needed 6. Test with: `flux get hr -n ` ### WHEN Modifying Existing Apps 1. Always read and update the README.md if configuration changes 2. Test changes don't break dependencies 3. Verify Flux reconciliation after changes 4. Document any new gotchas discovered ## Secret Management (CRITICAL) **SECURITY BOUNDARY**: All secrets must be encrypted with SOPS. ### Rules - ✅ All `*.sops.yaml` files MUST be encrypted with SOPS - ❌ NEVER commit unencrypted secrets - ✅ ALWAYS verify encryption before `git push` - ✅ Use Age encryption (not PGP) ### Environment Variables ```bash export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt" # or age.key in repo root export AGE_PUBLIC_KEY="age1..." # Public key for encryption ``` ### Commands Encrypt a new secret: ```bash sops --encrypt --in-place kubernetes/apps/namespace/app/secret.sops.yaml ``` Edit an encrypted secret: ```bash sops kubernetes/apps/namespace/app/secret.sops.yaml # Opens in $EDITOR, auto-encrypts on save ``` Decrypt for reading (don't save): ```bash sops --decrypt kubernetes/apps/namespace/app/secret.sops.yaml ``` Verify a file is encrypted: ```bash grep -q "sops:" kubernetes/apps/namespace/app/secret.sops.yaml && echo "Encrypted" || echo "UNENCRYPTED!" ``` ### Creating Secrets All Kubernetes secrets should use the SOPS encrypted format: ```yaml # secret.sops.yaml apiVersion: v1 kind: Secret metadata: name: my-secret namespace: my-namespace stringData: key: value ``` Then encrypt: `sops --encrypt --in-place secret.sops.yaml` ## Scripts Development Standards The `scripts/` directory contains utility scripts for local execution. ### Bash Scripts Requirements: - Must pass `shellcheck` validation - Include usage documentation in comments at top of file - Use proper error handling with `set -euo pipefail` - Use meaningful exit codes - Make scripts executable: `chmod +x script.sh` Example header: ```bash #!/usr/bin/env bash # Description: Brief description of what this script does # Usage: ./script.sh [options] set -euo pipefail ``` Validation: ```bash shellcheck scripts/my-script.sh ``` ### Python Scripts Requirements: - Give executable Python scripts a `uv` shebang, not a system interpreter: `#!/usr/bin/env -S uv run --script`, paired with a PEP 723 `# /// script` block declaring `requires-python` and `dependencies`. This makes the script self-contained and reproducible; `#!/usr/bin/env python3` picks up whatever interpreter and site-packages happen to be on `PATH`. - Use `uv` for dependency management - Use `ruff` for linting and formatting - Include docstrings and type hints - Follow PEP 8 style guide - If `uv run --script` + inline dependencies is not practical (for example, shared package/module layout, tooling limits, or runtime constraints), use the repository/project dependency approach and add a short note in the script header explaining why. Preferred standalone script pattern: ```python #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "requests>=2.32.0", # ] # /// """Brief description of what this script does.""" ``` Run: ```bash uv run scripts/my-script.py ``` Project-style setup with uv (fallback): ```bash uv venv source .venv/bin/activate uv pip install ``` Linting and formatting: ```bash ruff check scripts/my-script.py ruff format scripts/my-script.py ``` Example with type hints: ```python #!/usr/bin/env python3 """Brief description of what this script does.""" def main() -> None: """Main function.""" pass if __name__ == "__main__": main() ``` ## Testing & Validation ### Flux Health Checks Check all Flux sources: ```bash flux get sources git -A flux get sources oci -A ``` Check Kustomizations: ```bash flux get ks -A ``` Check Helm Releases: ```bash flux get hr -A ``` Force reconciliation: ```bash flux reconcile ks --with-source flux reconcile hr -n ``` ### Kubernetes Debugging Check pod status: ```bash kubectl get pods -n kubectl get pods -A # All namespaces ``` Describe resources: ```bash kubectl describe pod -n kubectl describe helmrelease -n ``` View logs: ```bash kubectl logs -n kubectl logs -n -f # Follow ``` Aggregate logs with stern: ```bash stern -n ``` Check events: ```bash kubectl get events -n --sort-by='.metadata.creationTimestamp' ``` ### Manifest Validation Kubeconform validation: ```bash ./scripts/kubeconform.sh ``` Pre-commit hooks: The repository has pre-commit hooks. Reference the `repository` recipes in the root `.justfile` to see what runs: ```bash pre-commit run --all-files ``` ## File Naming Conventions - **SOPS encrypted files**: `*.sops.yaml` (templates without secret values are `*.sops.yaml.tmpl`) - **Kustomization files**: `kustomization.yaml` or `ks.yaml` - **HelmRelease files**: `helmrelease.yaml` or `-helmrelease.yaml` - **App README**: Always `README.md` (not readme.md or README.MD) - **ConfigMaps/Secrets**: Descriptive names like `app-config.yaml`, `app-secret.sops.yaml` ## Secrets Management Naming Conventions (ESO vs SOPS) - Prefer 1Password/ESO as the primary secrets path; SOPS stays only for minimal bootstrap credentials. - 1Password vault: **homelab**. Keep all ExternalSecret targets in this vault for consistency and access control. - 1Password item titles: `{namespace}.{app}` (e.g., `default.ghost`); shared items use `shared.{purpose}`. - `op` references follow `op://homelab/./[section]/`. - Secret fields: `username`, `password`, `apiKey`, `token`, `certificate`, `privateKey` (avoid generic keys like `value`). - Kubernetes Secret names mirror 1Password item purpose where possible; avoid ambiguous names like `credentials` or `secret`. - ExternalSecret resource names: `{namespace}-{app}-{source}` (e.g., `default-ghost`). - ClusterSecretStore name: `onepassword`; ESO controller ServiceAccount: `external-secrets-controller` in the `external-secrets` namespace. ## Commit Message Convention All commit messages must follow the [Conventional Commits](https://www.conventionalcommits.org/) specification. ### Format ```text [optional scope][!]: [optional body] [optional footer(s)] ``` ### Rules **Type and Description:** - Allowed types: `feat`, `fix`, `build`, `chore`, `ci`, `docs`, `style`, `refactor`, `test`, `revert` - Use imperative, present tense (e.g., "add feature" not "added feature") - No trailing period on the description - Prefer lowercase unless referencing a proper noun - Keep the description concise and specific **Scope:** - Use a short, specific scope that identifies the area of change - Examples: `api`, `ui`, `deps`, `ci`, `docs`, `flux`, `ansible`, `networking` - Scope is optional but recommended for clarity **Breaking Changes:** - Mark breaking changes with `!` after the type/scope: `feat(api)!: change authentication method` - Include a `BREAKING CHANGE:` footer describing the impact and migration path **Body:** - Explain what and why, not how - Use bullet points for lists - Avoid repeating the subject line - Separate from subject with a blank line **Footers:** - Use `BREAKING CHANGE:` footer for breaking changes - Coding assistants MUST indicate their assistance via a commit message attribution: `AI-assistant: ` - Reference issues/PRs: `Closes #123`, `Refs #456`, `Fixes #789` ### Type Guidance - `feat`: New user-facing behavior or capability - `fix`: Bug fix that affects users - `docs`: Documentation only changes - `style`: Formatting changes that don't affect code meaning (whitespace, formatting, etc.) - `refactor`: Code changes that neither fix bugs nor add features - `test`: Adding or updating tests only - `build`: Changes to build system, dependencies, or tooling (e.g., npm, ansible roles) - `ci`: CI/CD configuration changes (e.g., GitHub Actions, Flux) - `chore`: Routine tasks, maintenance, or other changes that don't modify src or test files - `revert`: Reverts a previous commit ### Examples ```text feat(networking): add external ingress for blog service Configures HTTPRoute with external gateway and Cloudflare DNS annotation to make the blog publicly accessible. Closes #42 ``` ```text fix(cert-manager): resolve certificate renewal failures The ClusterIssuer was using an expired API token. Updated the secret with a new token and verified certificate issuance. Fixes #128 ``` ```text chore(deps): update flux to v2.3.0 Updated Flux components to latest stable release for security patches and performance improvements. ``` ```text feat(storage)!: migrate from openebs-hostpath to rook-ceph BREAKING CHANGE: All PVCs must be migrated manually. openebs-hostpath storage class is deprecated and will be removed in the next release. Migration guide: docs/storage-migration.md Refs #201 ``` ## Important Gotchas ### Security 1. **Always verify SOPS files are encrypted before committing** ```bash # Check all SOPS files are encrypted find . -name "*.sops.yaml" -exec grep -L "sops:" {} \; # Should return nothing if all are encrypted ``` 2. **Never commit the age.key file** (it's in .gitignore) 3. **Secrets in Ansible inventory** are also SOPS-encrypted (e.g., `host_vars/*.sops.yaml`) ### DNS & Networking 1. **Split DNS is required** for k8s-gateway to work - Home DNS server must forward `*.domain.com` to k8s-gateway IP - Without this, internal services won't resolve 2. **Certificate staging vs production** - Cluster starts with Let's Encrypt staging certificates - Switch to production once stable to avoid rate limits - Staging certs will show browser warnings 3. **Cloudflare tunnel** must be configured for external ingress to work - Check `kubernetes/apps/network/cloudflared/` for configuration ### Flux & GitOps 1. **Changes may take up to 30 minutes** to reconcile (default interval) - Force reconciliation for immediate updates 2. **Flux ignores some paths** defined in `kubernetes/flux/config/cluster.yaml` 3. **Bootstrap secrets** must exist in `kubernetes/bootstrap/` before Flux can decrypt other secrets 4. **Do NOT set `postBuild.substituteFrom` in an app `ks.yaml`.** The parent `flux-cluster` Kustomization (`kubernetes/flux/cluster/ks.yaml`) replaces it on every child (kustomize treats the CRD list as atomic), so it is silently dropped and live edits are reverted. Global vars (`SECRET_DOMAIN`, etc.) come from `cluster-secrets`; to add an app-specific secret, append it via a targeted JSON6902 patch in the parent's `spec.patches` (see `cloudflared` / `kube-prometheus-stack`), then reconcile the **parent** (`flux reconcile kustomization flux-cluster --with-source`), not the child. ### Cluster Operations 1. **Don't assume cluster is accessible** - verify connection before running kubectl commands: ```bash kubectl cluster-info ``` 2. **Node operations** require Ansible and SSH access to nodes 3. **Rook-Ceph operations** need special care - reference `kubernetes/apps/rook-ceph/mod.just` before making changes. YOU MUST CHECK WITH THE HUMAN BEFORE OPERATING DIRECTLY ON ROOK-CEPH. ### Kubeconfig - Located at `./kubeconfig` in repository root - Created by Ansible during k3s installation - Required for all `kubectl` and `flux` commands - **Provided automatically by direnv**: `direnv exec . ` exports `KUBECONFIG=./kubeconfig` from the devshell, so no manual `env` is needed ## Ansible Operations ### Inventory Structure ```text ansible/inventory/ ├── hosts.yaml # Main inventory ├── group_vars/ # Variables for groups │ ├── all/ │ ├── controller/ # Control plane nodes │ ├── kubernetes/ # All k8s nodes │ └── worker/ # Worker nodes └── host_vars/ # Per-host variables (SOPS-encrypted) ├── optiplex0.sops.yaml └── ... ``` ### Running Playbooks Check inventory: ```bash ansible-inventory -i ansible/inventory --list ``` Run a playbook: ```bash ansible-playbook -i ansible/inventory/hosts.yaml ansible/playbooks/.yaml --become ``` ### Common Playbooks - `k3s-install.yaml` - Install k3s on nodes - `k3s-prepare.yaml` - Prepare nodes for k3s (reboot required) - `k3s-nuke.yaml` - Completely remove k3s (destructive!) - `reboot.yaml` - Reboot nodes - `apt-update.yaml` - Update packages on nodes ## References - [README.md](./README.md) - Comprehensive setup and user documentation - [.codex/rules/kubectl.rules](.codex/rules/kubectl.rules) - Security rules for kubectl/flux commands - [docs/guides/02-ansible.qmd](./docs/guides/02-ansible.qmd) - Ansible usage details - [docs/guides/03-just.qmd](./docs/guides/03-just.qmd) - Just command reference (for humans) - [Flux Documentation](https://fluxcd.io/flux/) - Flux CD official docs - [k3s Documentation](https://docs.k3s.io/) - k3s official docs - [SOPS Documentation](https://github.com/getsops/sops) - SOPS usage and configuration --- Remember: This repository manages real infrastructure. Always test changes carefully, verify SOPS encryption, and understand the impact before committing.