hunter-party-go/invariant-hunter-go/SKILL.md
Audit Go code for weak invariants — unchecked errors, nil pointer risks, ignored context cancellation, unsafe type assertions, zero-value traps, panic/recover misuse, and missing validation at construction boundaries. Use when: tightening domain models, reducing panic risks, increasing error handling discipline, or establishing a safety baseline before refactoring.
npx skillsauth add skyosev/agent-skills invariant-hunter-goInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Audit Go code to make invariants explicit and enforced — catching unchecked errors, nil dereferences waiting to happen, type assertions without ok checks, zero-value traps, and places where panic replaces proper error handling. The goal: errors are always handled, nil is never dereferenced unexpectedly, and invalid states are caught at construction boundaries.
Errors are values, not exceptions. Every returned error must be handled — checked, propagated, or explicitly discarded with documented justification. An unchecked error is a silent failure.
Nil is the billion-dollar mistake in Go too. A nil pointer dereference panics at runtime. Validate at boundaries, return early on nil, and document nil semantics in function contracts.
Resolve at construction boundaries. Defaults and validation belong where data is created or enters the system —
constructor functions (New*), API handlers, config loaders. Downstream functions should require their inputs to be
valid. If every caller checks the same condition, push the check to the constructor.
Zero values must be safe or documented. Go initializes all variables to their zero value. If a struct's zero value is invalid (e.g., a nil map, zero ID, empty required field), the constructor must enforce valid construction, and this constraint must be documented.
Type assertions must be checked. A bare type assertion (x.(T)) panics on failure. Use the two-value form
(v, ok := x.(T)) or a type switch. Panic-on-assertion is acceptable only in test code or when the assertion
is provably correct.
Panic is for programmer errors, not runtime conditions. Panic for truly unreachable code, violated invariants in internal logic, or test failures. Never panic on user input, network errors, or recoverable conditions.
Context cancellation is not optional. Functions that accept context.Context must check for cancellation,
especially in loops, I/O operations, and long-running computations. Ignoring context defeats the purpose of passing
it.
recover is not error handling. Using recover to catch panics and continue is a code smell. It hides bugs.
The only legitimate use is at the top of goroutines in server code to prevent one request from crashing the process.
Not every finding requires action. Document these but do not flag as "must-fix":
| Pattern | When Acceptable |
| ------- | --------------- |
| _ = f() (discarded error) | Error is truly ignorable (Close on read-only file, logger flush) with comment |
| Bare type assertion in test | Test code where panic is the desired failure mode |
| Zero-value struct | Struct is explicitly designed for zero-value usability (documented) |
| recover in HTTP middleware | Top-level panic recovery to prevent server crash |
| panic in init() | Truly fatal configuration error during startup |
| panic in constructors for required fields | When the project explicitly endorses panic for invariant violations (check CLAUDE.md, AGENTS.md, or project conventions). Do not recommend checked-constructor alternatives when panic is the intentional policy |
Important: Before flagging panic/recover patterns, check the project's CLAUDE.md or equivalent for stated conventions. If the project says "panic is appropriate for invariant violations", respect that convention. Acknowledge it in the report and do not recommend alternatives unless the panic is reachable from untrusted input (user-facing API boundary).
Returned errors that are silently discarded or ignored.
Signals:
_ without justificationdefer f.Close() without checking the error (especially on writes)fmt.Fprintf(w, ...) in HTTP handlers without checking write errorif err != nil but the non-error path doesn't use the resultAction: Handle every error: return it, wrap it, or explicitly discard with a justifying comment. For defer Close,
use a named return to capture the error.
Code paths that can dereference nil without prior validation.
Signals:
(*T, error) where callers use *T without checking error firstok boolean when the zero value is ambiguous or could mask a missing
key (v := m[key] then using v where v == "" or v == 0 is indistinguishable from "not found")range over a nil slice is safe, but indexing a nil slice is not — check index operationsAction: Validate at the boundary where nil could enter. Return early or return error on nil. Document nil semantics in function contracts.
Type assertions without the two-value ok check, risking panics.
Signals:
x.(ConcreteType) without ok check (panics if wrong type)x.(A).field.(B) — multiple panic pointsinterface{} / any values from external sources (JSON, config, etc.)default case on an open interface (where new implementations can appear). Exhaustive type
switches on sealed/internal interfaces with a known finite set of types are not a concernAction: Use two-value form v, ok := x.(T) or type switch. Handle the failure case explicitly.
Structs whose zero value is invalid but can be created without a constructor.
Signals:
New* constructorsync.Mutex or sync.WaitGroup copied after first use (detected via go vet)Action: Provide a constructor. Make the struct unexported if zero-value construction is dangerous. Document zero-
value behavior. Use sync.Locker interface for mutexes that shouldn't be copied.
Poor error handling patterns that lose context or prevent errors.Is/errors.As matching.
Signals:
fmt.Errorf without %w (loses the error chain — cannot use errors.Is)Unwrap() methoderrors.Is / errors.Asvar ErrNotFound = "not found" (string, not error) or
var ErrNotFound error (nil, not initialized) instead of var ErrNotFound = errors.New("not found")fmt.Errorf("error: %w", err)Action: Use %w in fmt.Errorf for wrappable errors. Use errors.Is/errors.As for comparison. Add meaningful
context when wrapping: package name, operation, relevant parameters.
Ignoring context cancellation, improper context construction, or missing context propagation.
Signals:
context.Context but never checks ctx.Err() or ctx.Done() in loops/I/Ocontext.Background() or context.TODO() in non-main, non-test codecontext.WithCancel or context.WithTimeout without calling the cancel functionctx parameter in functions that make network/DB callsAction: Check context in loops and before expensive operations. Use context.TODO() only temporarily with a
comment. Pass context through the call chain. Always defer cancel functions.
Panics used for control flow or error handling instead of proper error returns.
Signals:
panic() called on user input validation failurepanic() in library code reachable by callers (not internal assertions)recover() used to suppress errors and continue normal executionlog.Fatal() or os.Exit() in library code (not main or init)must* functions that panic in non-test, non-init contextsAction: Replace panic with error return for any condition reachable by external callers. Use panic only for
genuinely unreachable code (exhaustive switch defaults, violated internal invariants). Limit recover to goroutine
crash prevention in server code.
Shared mutable state accessed without proper synchronization.
Signals:
sync.Mutex locked in one path but not in another that accesses the same statesync.WaitGroup.Add() called inside the goroutine instead of before goAction: Protect shared state with sync.Mutex, sync.RWMutex, sync/atomic, or channels. Run tests with
-race flag.
Resolve audit surface. The prompt may specify the scope as:
main/master)BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo main)
SCOPE=$(git diff --name-only $(git merge-base HEAD $BASE)...HEAD)
Constrain all subsequent scans to the resolved surface.
Check tooling configuration: go vet settings, staticcheck or golangci-lint config, race detection in CI.
Scan for patterns:
EXCLUDE='--glob !**/*_test.go --glob !**/vendor/** --glob !**/testdata/**'
# Unchecked errors (discarded with _)
rg '\b_\s*=\s*\w+\(' --type go $EXCLUDE
# Bare type assertions (no ok check)
rg --pcre2 '\.\(\*?\w+\)(?!\s*$)' --type go $EXCLUDE
# Panic calls
rg 'panic\(' --type go $EXCLUDE
# Recover calls
rg 'recover\(\)' --type go $EXCLUDE
# log.Fatal / os.Exit outside main
rg 'log\.Fatal|os\.Exit' --type go $EXCLUDE
# context.Background() / context.TODO() in non-main
rg 'context\.(Background|TODO)\(\)' --type go $EXCLUDE
# fmt.Errorf without %w
rg 'fmt\.Errorf' --type go $EXCLUDE
# Error string comparison
rg 'err\.Error\(\)\s*==|strings\.Contains\(err' --type go $EXCLUDE
# Nil map/channel field access
rg 'make\(map|make\(chan' --type go $EXCLUDE
# Deferred close without error check
rg 'defer\s+\w+\.Close\(\)' --type go $EXCLUDE
Produce counts by category, grouped by package.
For each function that returns error: Is every caller handling it?
For each _ = f(): Is the discard justified?
For each error wrap: Does it use %w? Does it add context?
For each pointer return: Do callers check error before using the pointer? For each map lookup: Is the ok value checked when the zero value is meaningful? For each interface parameter: Is nil handled or documented?
For each type assertion: Is the ok check present? For each type switch: Is there a default case? For each zero-value struct: Is the zero value safe or documented?
For each shared variable: Is it protected? For each goroutine: Is there a cancellation mechanism? For each sync primitive: Is it used correctly and consistently?
Save as YYYY-MM-DD-invariant-hunter-audit-{$LLM-name}.md in the project's docs folder (or project root if no docs folder exists).
# Invariant Hunter Audit — {date}
## Scope
- Surface: {diff / path / codebase}
- Files: {count or list}
- Exclusions: {list}
## Tooling Context
- `go vet`: {enabled/disabled in CI}
- Static analysis: {golangci-lint / staticcheck / none}
- Race detection: {enabled/disabled in CI}
## Baseline
| Category | Count |
| -------- | ----- |
| Discarded errors (`_ =`) | {n} |
| Bare type assertions | {n} |
| `panic()` calls (non-test) | {n} |
| `recover()` calls | {n} |
| `log.Fatal` / `os.Exit` in non-main | {n} |
| `context.Background()` in non-main | {n} |
| `fmt.Errorf` without `%w` | {n} |
| Error string comparison | {n} |
| Deferred Close without error check | {n} |
## Unchecked Errors
| # | Location | Call | Action |
| - | -------- | ---- | ------ |
| 1 | file:line | `_ = db.Close()` | Check error or add justification comment |
## Nil Pointer Risks
| # | Location | Pattern | Action |
| - | -------- | ------- | ------ |
| 1 | file:line | `user.Name` without nil check after `FindUser` | Check error before dereference |
## Type Assertion Safety
| # | Location | Assertion | Action |
| - | -------- | --------- | ------ |
| 1 | file:line | `val.(string)` | Use `val, ok := val.(string)` |
## Zero-Value Traps
| # | Type | Location | Issue | Action |
| - | ---- | -------- | ----- | ------ |
| 1 | `Config` | file:line | Nil map field, no constructor | Add `NewConfig()` constructor |
## Error Wrapping Issues
| # | Location | Pattern | Action |
| - | -------- | ------- | ------ |
| 1 | file:line | `fmt.Errorf("failed: %v", err)` | Use `%w` for unwrappable error |
## Context Misuse
| # | Location | Pattern | Action |
| - | -------- | ------- | ------ |
| 1 | file:line | `context.Background()` in service layer | Accept context from caller |
## Panic/Recover Misuse
| # | Location | Pattern | Action |
| - | -------- | ------- | ------ |
| 1 | file:line | `panic("invalid input")` | Return error instead |
## Race Condition Risks
| # | Location | Pattern | Action |
| - | -------- | ------- | ------ |
| 1 | file:line | Shared map without mutex | Add sync.RWMutex |
## Recommendations (Priority Order)
1. **Must-fix**: {unchecked errors on critical paths, nil dereference risks, bare type assertions on external data}
2. **Should-fix**: {error wrapping without %w, context misuse, panic in library code}
3. **Consider**: {zero-value documentation, deferred close error handling, race detection in CI}
file/path.go:line with the exact code.development
Transforms vague feature ideas into precise, codebase-grounded technical requirements. Use when requirements are ambiguous/incomplete, the user struggles to describe behavior, terminology is unclear, or multiple concepts are mixed. Output is a requirements spec—NOT an implementation plan.
tools
Audit TypeScript type definitions for design debt — duplicated shapes, missing derivations, over-engineered generics, under-constrained type parameters, reinvented utility types, and disorganized type architecture. Type structure and maintainability, not type enforcement. Use when: reviewing type definitions for maintainability, reducing type duplication, simplifying over-engineered type-level logic, or reorganizing type architecture after growth.
development
Audit TypeScript test code for quality gaps — missing coverage on critical paths, brittle tests coupled to implementation, over-mocking, assertion-free tests, missing edge cases, and duplicated test setup. Focuses on test effectiveness, not production code structure. Use when: reviewing TypeScript test suites for reliability, reducing false-positive test failures, improving coverage of critical business logic, or cleaning up test debt.
tools
Audit TypeScript class and interface design for SOLID violations — god classes, rigid extension points, broken substitutability, fat interfaces, and concrete dependency chains. Focuses on responsibility assignment and abstraction fitness. Use when: reviewing class hierarchies, preparing for extension with new variants, reducing coupling between services, or improving testability of class-heavy code.