Files
J3vbandClaude Fable 5 6432e65c56 chore(claude): session-start + pre-bash hooks, deny .env reads; durationcheck + noImplicitOverride (#1463)
* chore(claude): session-start and pre-bash hooks, deny .env reads

scripts/claude-hook.mjs, wired in .claude/settings.json:
- SessionStart warns when core.hooksPath is not .githooks, so a clone or a
  new machine cannot silently run without the repo git hooks.
- PreToolUse on Bash refuses a top-level cd: the tool's shell is persistent,
  so a cd leaks into every later command and a gate can report green from
  the wrong directory. Subshells, git -C and root-relative paths pass.

permissions.deny gains Read(**/.env): the gitignored env files never enter
the model's context. Server/.env.example stays readable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwaz4CHGAz85Rpypjvto5a

* chore(lint): durationcheck on the server, noImplicitOverride on the client

Both measured at zero hits on dev, so they cost nothing today and only
block regressions: a Duration multiplied by a Duration-typed value, and an
override left behind when its base method is renamed.

rowserrcheck and sqlclosecheck were measured too and rejected: their six
production hits are all correct code (rows.Err is checked inside
scanEventRows behind the rowsScanner interface; the three Close sites close
on every path by hand).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwaz4CHGAz85Rpypjvto5a

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 18:50:12 +00:00

127 lines
5.8 KiB
YAML

version: "2"
linters:
enable:
- gocritic # opinionated checks for bugs, performance, style
- gosec # security-focused static analysis (SQL injection, hardcoded creds, weak crypto)
- errcheck # ensures all errors are checked
- bodyclose # detects unclosed HTTP response bodies
- contextcheck # verifies context.Context propagation
- nilerr # detects returning nil when err is not nil
- prealloc # suggests pre-allocating slices for performance
- unconvert # removes unnecessary type conversions
- unparam # finds unused function parameters
- wastedassign # finds wasted assignments
- staticcheck # advanced static analysis (correctness, performance, deprecation)
- modernize # flags outdated idioms (slices/maps/min/max, range-over-int, any, fmt.Appendf)
# Complexity budgets. These are a RATCHET, not a standard: each threshold sits
# just above today's worst offender, so the build is green now and the budget
# only blocks regression past the current extreme. Tighten them over time — each
# step is a one-line edit here plus the refactor it forces. Measured values and
# the work each budget is waiting on are in the settings block below.
- funlen # function length (lines and statements)
- cyclop # cyclomatic complexity per function
- nestif # deeply nested if-blocks
- dupl # verbatim duplicated blocks
# Silent-drop and sentinel-error classes (added 2026-08-30).
- exhaustive # a switch over an enum-like type with no default that misses a member
- errorlint # errors.Is/As instead of == and type assertions on wrapped errors; %w in Errorf
- durationcheck # time.Duration multiplied by a Duration-typed value, e.g. d*time.Second where d is already a Duration
settings:
exhaustive:
# An explicit default branch is a deliberate catch-all; only switches without
# one, which fall through in silence, must list every member.
default-signifies-exhaustive: true
staticcheck:
checks:
- "all"
- "-SA1019" # suppress deprecated usage warnings (websocket library migration tracked separately)
gocritic:
enabled-tags:
- diagnostic
- performance
disabled-checks:
- hugeParam # too noisy for struct-heavy code
# Complexity budgets set to a TARGET, not to whatever passes today. Existing
# offenders are deliberately left failing rather than excluded: an exclusion
# list goes stale and quietly becomes permanent, whereas a failing check is a
# backlog you can see. Measured 2026-08-16 over 1446 production functions,
# tests excluded.
#
# These are not the tool defaults (60/40, 10, 4). The defaults descend from
# 1976-era cyclomatic-complexity work that predates Go's explicit error
# handling: every `if err != nil` costs a branch, so idiomatic Go scores high
# for no real complexity. That is why golangci-lint's other cyclomatic linter,
# gocyclo, defaults to 30 rather than 10. The values below are chosen for a Go
# server rather than copied from C-era literature.
funlen:
# funlen counts in its own units — it ignores blank lines and comments, so
# main.go run() is 311 here and 422 by raw line span. Worst today: 311 lines,
# 131 statements.
lines: 100
statements: 50
cyclop:
# Worst today: ws/voice_join.go handleVoiceJoin at 59, main.go run at 57,
# plugin/registry.go InstallFromZip at 44.
max-complexity: 20
nestif:
# Worst today: 17.
min-complexity: 8
dupl:
# The conventional value. Flags three real duplicate pairs, all adjacent
# near-identical siblings worth collapsing:
# db/mention_queries.go:322-357 vs :364-399
# ws/voice_controls.go:13-42 vs :45-73
# ws/voice_controls.go:76-121 vs :124-171
threshold: 150
gosec:
excludes:
- G104 # unhandled errors — errcheck covers this better
- G304 # file path from variable — expected in file storage code
- G306 # WriteFile perms ≤0600 — our only hits are generated source files (cmd/genprotocol), which must stay world-readable or multi-stage container builds break
- G706 # log injection — false positive with slog structured logging (values are typed key-value pairs, not interpolated)
exclusions:
# Suppress noisy linters in test files
rules:
- linters: [unparam]
path: _test\.go
- linters: [gosec]
path: _test\.go
- linters: [errcheck]
path: _test\.go
# Complexity budgets apply to production code only. Table-driven tests are
# legitimately long, and duplicated setup between test cases is clearer than
# a shared helper that hides what each case is doing.
- linters: [funlen, cyclop, nestif, dupl]
path: _test\.go
# gofmt is the required Go formatting gate (S-05). It lives here rather than as a
# standalone CI step because `gofmt -l` prints offenders and still exits 0, so a
# `gofmt -l` step cannot fail a build. Running it as a formatter inside
# golangci-lint means it reports through the already-pinned Lint check.
#
# In v2 the formatters moved out of `linters.enable` into their own section, and
# they carry their own exclusion list.
formatters:
enable:
- gofmt
exclusions:
paths:
# sqlc output, verified by `git diff --exit-code db/dbgen` after regeneration.
- db/dbgen
# Report every finding. The defaults hide work three separate ways, which is fine
# when the list is meant to be empty and actively misleading when it is a backlog:
# "fixed everything shown" would leave more behind. uniq-by-line is the sharp one —
# it keeps a single issue per line, and since cyclop and funlen both anchor at the
# function declaration, enabling cyclop silently swallowed 16 of funlen's 21
# findings here.
issues:
max-issues-per-linter: 0
max-same-issues: 0
uniq-by-line: false