template/CLAUDE.md

8.3 KiB

CLAUDE.md — Agent Instructions

This file is read automatically by Claude Code at the start of every session. Keep it concise — the agent needs signal, not essays.


Project overview

Go 1.25 template for PoC, hobby projects, and small publishable packages. Demonstrates: structured logging (slog), config (flag), consumer-defined interfaces + manual fakes, result type (happy-path error handling), linting (golangci-lint), security scanning (gosec, govulncheck), git hooks, devcontainer, VSCode tasks.

Key constraint: go.mod stays free of dev tool deps (tools are pinned in tools.versions and run via go run tool@version) so packages published from this repo have a clean module graph for consumers.

Module: gitea.djmil.dev/go/template — update this when you fork.


Design philosophy

The Twelve-Factor App is the primary reference for default implementation choices. Follow its recommendations unless circumstances force a deviation — and document the reason when you do.

Key factors that shape this codebase most directly:

  • Logs (factor XI) — the app writes log events to stderr as an unbuffered stream. It never opens log files or manages rotation. The execution environment (shell, systemd, Docker, k8s) routes and stores the stream. Human-readable program output goes to stdout (fmt.Print*).
  • Config (factor III) — all configuration comes from the environment (flags in this template; env vars are the 12-factor ideal). No hard-coded values in logic packages.
  • Dependencies (factor II) — explicit declaration; dev tools are pinned in tools.versions and kept out of go.mod so published packages have a clean module graph.

Project structure

cmd/app/main.go          composition root — wires deps, no logic here
internal/config/         flag-based config loader (config.Load)
internal/logger/         slog wrapper with WithField / WithFields
internal/greeter/        Example domain package (delete or repurpose)
pkg/result/              Example publishable package (Result/Expect types)
tools.versions           Pinned tool versions (sourced by Makefile and pre-push hook)
.golangci.yml            Linter rules
.githooks/pre-push       Runs gofmt + go vet + golangci-lint + gosec before push

Project rules

  • Module imports — always use the full module path gitea.djmil.dev/go/template/...
  • Packages — keep cmd/ thin (wiring only); business logic belongs in internal/
  • Types — expose concrete types from constructors (New(...) *Type); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free)
  • Errorspkg/result is a convenience tool for removing error-threading clutter from application logic; use it as follows:
    • pkg/ libraries only return result.Expect[T] — never call .Expect(), .Must(), or .Expectf() inside library code; those methods exit the goroutine via runtime.Goexit and are only safe in application-layer code protected by a boundary
    • application code (cmd/, HTTP handlers, etc.) chains .Expect("context") freely — each call exits the goroutine on failure and is caught at the entry point
    • top-level entry points defer result.Catch(&err) (or use result.Run(...)) to convert any result exit into a normal Go error; genuine runtime panics (nil-deref, etc.) are re-panicked
    • result.Catch is incompatible with -tags result_goexit: it relies on recover() which cannot intercept runtime.Goexit; prefer result.Run/result.Go which work in both builds
    • bridge existing (T, error) stdlib/third-party calls with result.Of(...): result.Of(os.ReadFile("cfg.json")).Expect("read config")
    • use result.StackTrace(err) to retrieve the capture-site stack from a caught error
    • still use fmt.Errorf("context: %w", err) when wrapping errors before constructing a result.Fail
  • Logging — logs go to stderr (structured, machine-readable, per 12-factor XI); human output goes to stdout via fmt.Print*. Use log.WithField("key", val) for structured context; never fmt.Sprintf in log messages; log/slog is the backend
  • Config — all configuration through internal/config (flag-parsed); no hard-coded values in logic packages

Code style

  • Follow gofmt + goimports formatting (enforced by linter and git hook)
  • Imports: stdlib → blank line → external → blank line → internal (goimports handles this)
  • Error variables: err for local, ErrFoo for package-level sentinels
  • Constructors: New(deps...) *Type pattern
  • Comment every exported symbol (golangci-lint will warn if missing)
  • Max line length: 120 chars (configured in .golangci.yml)
  • Prefer explicit over clever; PoC code should be readable first

Testing rules

  • Tests use only the standard testing package — no third-party assertion libraries
  • Test files: package foo_test (black-box) unless white-box access is needed
  • Fake dependencies with manual fakes (implement the interface inline in _test.go)
  • Use logger.NewNop() when the test doesn't care about log output
  • Use logger.NewWriter(&buf, "debug") in acceptance / integration tests that need to assert on log content
  • Table-driven tests with t.Run("description", ...) for multiple cases
  • The race detector is enabled in CI (make test-race); don't introduce data races
  • Never use time.Sleep in tests; use channels or t.Cleanup
  • Use internal/testutil helpers instead of manual checks — ResultOk, ResultOkNotNil, ResultErr for result.Expect[T]; NoError, Error, ErrorContains, Equal for plain values

Development commands

make init          # first-time setup: fetch deps, configure git hooks
make tools         # install tool binaries to GOPATH/bin (versions from tools.versions)
make build         # compile to ./bin/app
make run           # go run with default flags
make test          # run all tests
make test-race     # tests + race detector
make lint          # go vet + golangci-lint
make lint-fix      # go fix + golangci-lint auto-fix
make security      # gosec + govulncheck
make release       # list releases, or tag+push after full checks (make release VERSION=v0.1.0)
make clean         # remove bin/

VSCode: Ctrl+Shift+B = build, Ctrl+Shift+T = test. Debug: use launch config "Debug: app" (F5).


Adding new features (checklist)

  1. Write the implementation in internal/<domain>/ — return a concrete *Type, no interface at the implementation site
  2. In the consumer package (or _test.go), declare a minimal interface covering only the methods you call
  3. Write unit tests using a manual fake that satisfies that interface
  4. Wire the concrete type in cmd/app/main.go
  5. Run make lint test before committing

Known pitfalls

  • govulncheck makes network calls; excluded from pre-push hook (run manually)
  • Tool versions live in tools.versions — edit that file to upgrade, both Makefile and hook pick it up
  • go run tool@version is used in lint/security targets; Go caches downloads so subsequent runs are fast
  • make tools installs binaries to GOPATH/bin for IDE integration (e.g. dlv for the debugger)
  • go fix rewrites source files; run make lint-fix before committing after a Go version bump

Recent work

  • 2026-03-29 — Stripped to stdlib-only: removed testify/mockery→manual fakes, zap→slog, viper→flag.
  • 2026-03-29 — Pre-commit hook moved to pre-push; go vet + go fix added to lint pipeline.
  • 2026-03-29 — Fixed stale docs and .golangci.yml local-prefixes; .vscode launch configs use CLI flags.
  • 2026-04-01 — Replaced tools.go/go.mod pinning with tools.versions + go run tool@version; go.mod is now free of dev tool deps.
  • 2026-04-01 — Added make release: lists tags with no args; validates semver, runs test-race+lint+security, then tags+pushes.
  • 2026-04-23 — Documented result layering rule: pkg/ libraries only return Expect[T]; .Expect()/.Must() calls belong in application-layer code.