template/CLAUDE.md
2026-04-07 20:32:27 +00:00

5.6 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.


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)
  • Errors — wrap with fmt.Errorf("context: %w", err); never swallow errors silently
  • Logging — 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
  • 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

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.