# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this is A Kubernetes operator for managing egress proxies — currently an empty scaffold with no controller code yet. Intended shape: a controller-runtime manager reconciling custom resources (or existing workload types) to provision/configure egress proxy infrastructure. TODO: confirm the actual CRD(s)/API group once implemented. ## Commands No Makefile, linter, or entrypoint exists yet. Verified state as of bootstrap: ```bash go build ./... # nothing to build yet — no .go source files exist go test ./... # no test files exist yet ``` No linting or formatting tools are configured in this project yet — `golangci-lint` and `gofumpt` are pre-approved in `.claude/settings.json` for when they're wired up, but neither has a config file or CI step yet. There is no `main.go` or `cmd/` entrypoint yet, so there's nothing to run locally. ## Architecture Not yet built. TODO: once the controller-runtime manager and reconcile loop exist, document the data-flow diagram and package/role table here, following the standard reconcile-loop shape: fetch → resolve → list related → compute desired state → merge → patch only on diff. ## Conventions ### Go Conventions - Use the latest stable Go version (check https://go.dev/dl/ before starting a new feature). - Use `slog` for structured logging. - `context.Context` is the first parameter of every function that does I/O or can be cancelled. - No `init()` unless there is no other option. - Error wrapping: `fmt.Errorf("...: %w", err)` for single errors; `errors.Join` for multiple. - Prefer stdlib over third-party dependencies where reasonable. - No vendoring unless explicitly requested. - Fail fast, return early. - No obvious comments. ### Testing **All new Go code must have corresponding tests.** - Use stdlib `testing` — no testify unless already present in the module. - Table-driven tests are the default pattern. - Test files live next to source: `foo.go` → `foo_test.go`. - Name tests `Test_` (e.g. `TestParseRoster_emptyInput`). - Use `t.Parallel()` where safe. - Use subtests for table entries: `t.Run(tc.name, func(t *testing.T) { ... })`. - Test behaviour, not implementation. - Guard slow tests with `testing.Short()`: `if testing.Short() { t.Skip() }`. - Integration tests that need real infra (DB, network) go behind `//go:build integration` and use `testcontainers-go` rather than mocking the infra away. #### Useful test commands ```bash go test ./... # all unit tests go test -v ./... # verbose go test -cover ./... # with coverage go test -short ./... # skip slow/integration tests go test -v ./internal/... # specific package tree go test -race -v -run TestName ./path/to/package # single test, race detector on go test -tags=integration ./... # integration tests (requires infra) ``` ### Plans When Claude Code's plan mode is used, save the plan file inside the repo at `docs/plans/YYYY-MM-DD-HHMM-.md` instead of the default `~/.claude/plans/` location. Get the timestamp with `date "+%Y-%m-%d-%H%M"`. The `` is a short kebab-case summary of the plan's topic. Include the same timestamp in the plan's header, e.g.: # Plan: **Created:** 2026-08-07 15:30 Create `docs/plans/` on first use. Plan files are committed to the repo so other contributors can review historical decisions. **Ordering matters:** plan mode itself restricts writes to the default `~/.claude/plans/` location — that's a hard tool restriction during plan mode, not a choice. So the copy into `docs/plans/` cannot happen until *after* `ExitPlanMode`. When it happens, it must be the very first action taken post-exit, committed on its own, *before* any implementation work starts (branching, scaffolding, editing code). Do not fold the plan-file commit into the first implementation commit, and do not defer it until "later" in the same turn — treat "save and commit the plan" as its own checkpoint that blocks starting Step 0. ### Plan execution summaries After finishing each step of an approved plan, append a short, human-readable summary to `docs/plans-executions/YYYY-MM-DD-HHMM-<slug>.md` — same timestamp and slug as the originating plan in `docs/plans/`, so the two files pair up 1:1. Create the file (and `docs/plans-executions/` on first use) after the first step completes; append a new section per step after that, in chronological order (bottom of the file), not newest-first like `CHANGELOG.md`. **Status checklist.** The top of the file, above the first step's entry, is a markdown checklist with one line per step from the plan (`- [ ] Step 0 — <title>`), so a single glance shows what's done and what's left without reading prose or cross-referencing `git log`. Check a box in the same commit that adds that step's entry — never leave a completed step unchecked or a not-yet-done step checked. This checklist is the authoritative status record; an in-session todo list (e.g. `TodoWrite`) is ephemeral and does not substitute for it — a new session resuming this plan should be able to read this file alone and know exactly where to pick up. Keep the prose light — a few sentences on what was done, plus anything genuinely interesting or non-obvious about how it went (a judgment call made, a spec gap found, something that didn't work as expected). This is not a duplicate of the plan or the diff; skip steps that went exactly as planned with nothing worth flagging. **Include exact commands.** Any invocation of an external tool that isn't a plain file edit — CLI scaffolding tools (`kubebuilder`, code generators), package installs, other non-obvious shell commands — goes in as a fenced code block with the exact command-line actually run, not a paraphrase. Prefer the real invocation over a description of it; a future reader (or a future Claude) should be able to copy the snippet and reproduce the step. Trim noisy stdout, but keep anything that changed the outcome (a flag that mattered, an unexpected error, a version that got picked automatically). **Structure each command snippet with one line of context before it** — why it was run that way, not just that it was run (e.g. "installed into a scratch `GOBIN` because the plan's install path 404s" beats a bare command with no framing). When something deviated from what the plan said — a flag correction, a version resolved differently than expected, a tool auto-running another tool — call that out explicitly rather than presenting the final working command as if it were the first thing tried. Close the entry with a short "worth noting" paragraph covering anything a future reader should know before touching this step's output: defaults that came out differently than planned, files outside the plan's scope that got touched, things deliberately left for a later step. Use the Step 0 entry in `docs/plans-executions/2026-08-07-1747-proxy-operator.md` as the reference example for the level of detail expected. Commit the summary update together with that step's implementation commit. ### Changelog Maintain a running changelog in `CHANGELOG.md` at the repo root. After every significant change, fix, or update — once the user confirms it works — append a new entry **at the top** of the file in this format: ## YYYY-MM-DD HH:MM TZ — short title - One-line summary of what changed and why. - Key files touched (optional, only if useful for traceability). Get the timestamp with `date "+%Y-%m-%d %H:%M %Z"`. Never write a literal placeholder like `HH:MM` — always run `date` first to get the real current time. Skip trivial edits (typos, formatting, comment tweaks); only log changes a future reader would care about. ### Branching & Merge Requests The remote is Gitea (`gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator`). For **features**, do not commit to `main` directly. Use a branch + merge request flow: 1. **Create a branch off `main`** before starting work: - `feat/<slug>` for features (e.g. `feat/roster-import`) - `fix/<slug>` for bug-fix branches the user explicitly asks for - `<slug>` is short kebab-case 2. **Commit on the branch** following the commit conventions below. 3. **Push the branch** to `origin` with `-u` so it tracks. 4. **Open the MR with `tea`** rather than printing a compare URL: ```bash tea pr create \ --title "<short title>" \ --description "<body>" \ --base main \ --head <branch> ``` `tea` is already authenticated against the Gitea instance; just run it. Print the resulting PR URL for the user. If `tea` is unavailable, fall back to printing the compare URL (`gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/compare/main...<branch>`) and let the user open the MR manually. 5. **Do not merge or delete the branch** from the CLI — neither via `tea`, `gh`, nor `git push --delete`. The user does that in Gitea. **Exceptions — committing straight to `main` is fine for:** - Small bug fixes / hotfixes the user describes as such. - Typo / comment / formatting tweaks. - Edits the user explicitly says to push to `main`. When uncertain whether something is a feature or a small fix, ask before branching. ### Git Commits Always append a `Co-Authored-By` trailer to indicate AI assistance: Co-Authored-By: Claude <noreply@anthropic.com> ### CI/CD `.gitea/workflows/build.yaml` builds the manager image and pushes it to the Gitea registry. Triggers: any tag push, or manual `workflow_dispatch` with a `tag` input. A lightweight `check` job (`go vet` / `go build` / `go test -short`) gates the build. - Images: `gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator:<tag>` plus an immutable `sha-<12-char-commit>` tag on every build; `:latest` moves only on real tag pushes, never on manual dispatch. - Requires the `REGISTRY_TOKEN` repo secret (Gitea PAT with `write:package`), same convention as the other projects on this Gitea instance. - The commit is baked into the binary via the `GIT_COMMIT` build arg (see Dockerfile). ## Gotchas - This repo currently contains only `go.mod` (module `gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator`) — no controller code, `main.go`, Dockerfile, k8s manifests, Makefile, or CI config exist yet. Nothing in this file describes working software; it's the convention scaffold to build against.