2026-01-13 21:07:14 -08:00
# CLAUDE.md
2026-02-04 07:09:28 -08:00
Guidance for Claude Code working in this repository. See also [[ai-assistance-guide]].
2026-01-13 21:07:14 -08:00
2026-02-04 07:09:28 -08:00
## Overview
2026-01-13 21:07:14 -08:00
2026-02-04 07:09:28 -08:00
blumeops is Erich Blume's GitOps repository for personal infrastructure, orchestrated via tailnet `tail8d86e.ts.net` .
2026-01-13 21:07:14 -08:00
2026-02-04 07:09:28 -08:00
**CRITICAL: Public repo at github.com/eblume/blumeops - never commit secrets!**
2026-01-15 09:02:27 -08:00
2026-02-17 10:18:20 -08:00
**Shell:** The user's shell is **fish ** . Use `$status` not `$?` for exit codes. Use fish syntax in interactive examples.
2026-01-16 18:47:47 -08:00
## Rules
2026-01-13 21:07:14 -08:00
2026-02-25 17:42:47 -08:00
1. **Always run `mise run ai-docs` at session start **
2026-02-04 07:23:12 -08:00
This will refresh your context with important information you will be assumed to know and follow.
2026-02-25 17:42:47 -08:00
**Read the full output ** — never truncate, pipe to `head` /`tail` , or skip sections.
2026-02-19 14:38:21 -08:00
2. **Always use `--context=minikube-indri` with kubectl ** (or `--context=k3s-ringtail` for ringtail services) - work contexts must never be touched
2026-02-23 16:19:54 -08:00
3. **Classify the change as C0/C1/C2 before starting ** (see below) — this determines branching and PR requirements
4. **Feature branches + PRs for C1/C2 ** - checkout main, pull, create branch, open PR via `tea pr create` . C0 goes direct to main.
2026-02-04 07:09:28 -08:00
5. **Check PR comments with `mise run pr-comments <pr_number>` ** before proceeding
2026-03-03 15:30:00 -08:00
6. **Add changelog fragments (all change levels) ** - `docs/changelog.d/<name>.<type>.md`
2026-02-04 07:23:12 -08:00
Types: `feature` , `bugfix` , `infra` , `doc` , `ai` , `misc`
2026-03-03 13:15:06 -08:00
Applies to C0, C1, and C2 whenever the change is user-visible or noteworthy.
2026-03-03 15:30:00 -08:00
- **C1/C2:** Use branch name: `<branch>.<type>.md`
- **C0:** Use orphan prefix: `+<descriptive-slug>.<type>.md` (avoids `main.*` collisions)
2026-02-04 07:09:28 -08:00
7. **Test before applying ** - dry runs (`--check --diff` ), syntax checks, `ssh indri '...'`
2026-02-23 16:19:54 -08:00
8. **Wait for user review before deploying ** (C1/C2)
9. **Never merge PRs or push to main without explicit request ** (C0 commits to main are fine)
2026-02-04 07:49:15 -08:00
10. **Verify deployments ** - `mise run services-check`
2026-01-28 20:43:51 -08:00
2026-02-20 08:15:20 -08:00
## Change Classification
Before starting work, classify the change:
2026-02-23 16:19:54 -08:00
| Class | Name | When to use | Key trait |
|-------|------|-------------|-----------|
| **C0 ** | Quick Fix | Small, low-risk, fix-forward safe | Direct to main, no PR |
| **C1 ** | Human Review | Moderate complexity or risk | Feature branch + PR, docs-first |
| **C2 ** | Mikado Chain | Multi-phase, multi-session, high complexity | Mikado Branch Invariant |
**C0** — commit directly to main. No branch or PR needed. Fix forward if problems arise.
**C1** — feature branch with early PR. Search related docs first, write documentation changes before code, deploy from the unmerged branch (ArgoCD `--revision` , Ansible from checkout). Upgrade to C2 if complexity spirals.
**C2** — branch `mikado/<chain-stem>` governed by the Mikado Branch Invariant: all card commits first, then code progress, then card closures. Commits use `C2(<chain>): plan/impl/close/finalize` convention. Reset the branch when new prerequisites are discovered. Resume with `mise run docs-mikado --resume` .
2026-02-20 08:15:20 -08:00
See [[agent-change-process]] for the full methodology.
2026-01-20 14:55:37 -08:00
## Project Structure
```
2026-02-04 07:09:28 -08:00
./docs/ # documentation (Diataxis, Quartz)
./docs/changelog.d/ # towncrier fragments
2026-02-17 07:54:34 -08:00
./.dagger/ # dagger pipelines
./.forgejo/ # forgejo-runner actions and workflows
2026-02-04 07:09:28 -08:00
./mise-tasks/ # scripts via `mise run`
./ansible/playbooks/ # ansible (indri.yml primary)
./ansible/roles/ # indri service roles
./argocd/apps/ # ArgoCD Application definitions
./argocd/manifests/ # k8s manifests per service
2026-02-17 07:54:34 -08:00
./fly/ # fly.io proxy for public routing
./pulumi/ # Pulumi IaC (tailnet ACLs, dns, cloud)
~/.config/{nvim,fish} # user's shell config, managed by chezmoi
2026-02-04 07:09:28 -08:00
~/code/personal/ # user's projects
2026-02-17 07:54:34 -08:00
~/code/personal/zk # user's Obsidian-sync managed zettelkasten. Potential source for reference data.
2026-02-04 07:09:28 -08:00
~/code/3rd/ # mirrored external projects
~/code/work # FORBIDDEN
2026-01-16 18:47:47 -08:00
```
2026-02-20 08:15:20 -08:00
Other code paths will be listed via ai-docs, this is just an overview. When you
2026-02-17 07:54:34 -08:00
encounter wiki-links (`[[like-this]]` ) it is referring to docs/ cards.
2026-01-20 14:55:37 -08:00
## Service Deployment
2026-02-04 07:09:28 -08:00
### Kubernetes (ArgoCD)
2026-01-20 14:55:37 -08:00
2026-03-11 18:37:31 -07:00
Most services run in minikube on indri via ArgoCD (app-of-apps, manual sync). GPU workloads (Frigate, ntfy) run on ringtail's k3s cluster, also managed by ArgoCD.
2026-01-20 14:55:37 -08:00
2026-02-04 07:09:28 -08:00
**PR workflow:**
1. Create branch, modify `argocd/manifests/<service>/`
2026-02-17 07:54:34 -08:00
2. Push. Sync 'apps' app if service definition changed (set --revision to branch).
2026-02-04 07:09:28 -08:00
3. Test on branch: `argocd app set <service> --revision <branch> && argocd app sync <service>`
4. After merge: `argocd app set <service> --revision main && argocd app sync <service>`
2026-01-23 17:00:12 -08:00
2026-02-04 07:09:28 -08:00
**Commands:** `argocd app list|get|diff|sync <app>`
2026-01-20 14:55:37 -08:00
2026-02-10 13:09:55 -08:00
**Login:** `argocd login argocd.ops.eblu.me --username admin --password "$(op read 'op://vg6xf6vvfmoh5hqjjhlhbeoaie/srogeebssulhtb6tnqd7ls6qey/password')"`
2026-02-04 07:09:28 -08:00
### Indri (Ansible)
Native services: Forgejo, Zot, Caddy, Borgmatic, Alloy
2026-01-20 14:55:37 -08:00
```fish
2026-02-04 07:09:28 -08:00
mise run provision-indri # full
mise run provision-indri -- --tags <role> # specific
mise run provision-indri -- --check --diff # dry run
2026-01-13 21:12:24 -08:00
```
2026-01-14 13:23:05 -08:00
2026-02-04 07:09:28 -08:00
### Routing
2026-01-20 14:55:37 -08:00
2026-02-04 07:09:28 -08:00
| Domain | Mechanism | Reachable from |
|--------|-----------|----------------|
Restrict flyio-proxy ACLs to dedicated tag:flyio-target endpoints (#126)
## Summary
- Introduce `tag:flyio-target` so services must explicitly opt in to be reachable by the fly.io proxy
- Replace broad `tag:k8s` and `tag:homelab` grants with the new tag in the ACL rule and test
- Add `tailscale.com/tags: "tag:k8s,tag:flyio-target"` annotation to docs, loki, and prometheus Ingresses
- Switch Alloy push endpoints from `*.ops.eblu.me` (Caddy) to `*.tail8d86e.ts.net` (Tailscale Ingress)
- Update docs: flyio-proxy, caddy, tailscale, forgejo (future public access + security checklist), expose-service-publicly
## Manual step (not in PR)
Update the k8s operator OAuth client in the Tailscale admin console to include `tag:flyio-target` in its scope. Without this, the operator cannot assign the new tag to Ingress proxy nodes.
## Deployment order
1. **Pulumi ACLs** — `mise run tailnet-preview && mise run tailnet-up`
2. **OAuth client** — Manual update in Tailscale admin console
3. **K8s Ingresses** — `argocd app sync apps && argocd app sync docs loki prometheus`
4. **Fly.io proxy** — `mise run fly-deploy`
5. **Verify** — `mise run services-check`, check Grafana dashboards
## Test plan
- [ ] `mise run tailnet-preview` shows clean diff
- [ ] `argocd app diff docs`, `argocd app diff loki`, `argocd app diff prometheus` show only annotation additions
- [ ] After deploy: Grafana dashboards show continued log/metric flow
- [ ] `curl -sf https://docs.eblu.me` returns 200
- [ ] `mise run services-check` passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/126
2026-02-08 21:54:18 -08:00
| `*.eblu.me` | Fly.io proxy (Tailscale tunnel) | public internet |
| `*.ops.eblu.me` | Caddy on indri | k8s pods, containers, tailnet |
2026-02-04 07:09:28 -08:00
| `*.tail8d86e.ts.net` | Tailscale MagicDNS | tailnet clients only |
2026-01-20 14:55:37 -08:00
2026-02-04 07:09:28 -08:00
Check tailscale serve: `ssh indri 'tailscale serve status --json'`
2026-01-20 14:55:37 -08:00
2026-02-04 07:09:28 -08:00
## Container Releases
2026-01-24 13:30:26 -08:00
```fish
2026-02-04 07:09:28 -08:00
mise run container-list # show images/tags
mise run container-release <name> <version> # tag and build
2026-01-24 13:30:26 -08:00
```
2026-02-17 07:54:34 -08:00
The goal is to eventually use only locally built containers in all cases, with
full supply chain control via forge.ops.eblu.me repositories, mirroring source
from upstream.
2026-01-24 13:30:26 -08:00
2026-01-17 17:34:53 -08:00
## Third-Party Projects
2026-02-04 07:09:28 -08:00
Ask user to mirror on forge first, then clone to `~/code/3rd/<project>/` .
2026-01-17 17:34:53 -08:00
2026-01-16 18:47:47 -08:00
## Task Discovery
2026-01-14 13:23:05 -08:00
2026-01-16 18:47:47 -08:00
```fish
2026-02-04 07:09:28 -08:00
mise run blumeops-tasks # fetch from Todoist, sorted by priority
2026-01-14 13:23:05 -08:00
```
2026-02-17 07:54:34 -08:00
Most tasks are stored in `./mise-tasks/` . For scripts with any logic or
complexity, use uv run --script 's with explicit dependencies. Complex
workflows with artifacts should become dagger pipelines. Mise tasks are for
development processes and operations - tools for the user or the agent.
2026-01-14 13:23:05 -08:00
2026-01-16 18:47:47 -08:00
## Credentials
2026-02-17 07:54:34 -08:00
Root store is 1Password. Never grab directly - use existing patterns (ansible
pre_tasks, external-secrets, scripts with `op` CLI). It's ok to use `op item
get` without ` --reveal` to explore what secrets are available, however.
2026-02-10 13:09:55 -08:00
2026-02-17 07:54:34 -08:00
Prefer `op read "op://vault/item/field"` over `op item get --fields` to avoid
quoting issues with multi-line values.