Guidance for AI coding agents working in this repository (github.com/kolide/launcher). These are the authoritative Go conventions for generated code and reviews.
For concrete usage, look at current code in the repo rather than relying on inline snippets here — examples drift, the code does not. Grep for the relevant symbol (e.g.
gowrapper.Go,rungroup.NewRunGroup,tablewrapper.New) to find live, correct examples.
- Build paths with
filepath.Joinin any file that compiles on multiple platforms (plain.gofiles,_unix.go). Never concatenate separators manually (nobase + "/" + name). - Use
path/filepath, notpath, for filesystem paths —pathis slash-only and meant for URLs. - Hardcoded literal paths are acceptable only in single-platform files (
_darwin.go,_windows.go,_linux.go, or behind a//go:build <os>constraint), especially when copied verbatim from vendor docs.
-
goroutines (required)
- Do use
ee/gowrapper.Go(ctx, slogger, func(){ ... })orGoWithRecoveryActioninstead of rawgo func(). - Never launch raw goroutines; they are forbidden in
.golangci.ymland must be wrapped for panic logging and recovery. - Always pass a meaningful
context.Contextand a*slog.Logger.
- Do use
-
early returns and error handling
- Prefer guard clauses and early returns to reduce nesting.
- Wrap and propagate errors with context using
%w, e.g.,fmt.Errorf("creating X: %w", err). - Do not use
panicoros.Exit(forbidden by lints). Log errors withslogand return them. - Keep functions free of naked returns unless trivial.
-
platform-specific code organization
- Do use separate files with build constraints for platform-specific implementations.
- Never use
runtime.GOOSchecks for code that won't compile on all platforms. - Split into
_posix.go(for macOS + Linux), and otherwise_linux.go,_windows.go,_darwin.gofiles when using platform-specific packages.
-
temporary file lifecycle management
- Do use
agent.MkdirTemp()for temporary files with appropriate prefixes. - Do defer cleanup with error logging if cleanup fails.
- Never leave temporary files without explicit cleanup.
- Do use
-
system command execution
- Do use
ee/allowedcmdwrappers instead of directexec.Command. - Do pass context for cancellation and timeouts.
- Never use raw
exec.Command(forbidden byforbidigolinter).
- Do use
-
table-driven tests (required pattern)
- Do use table-driven tests for any function with multiple input/output scenarios.
- Do use
t.Run(tt.name, ...)to create named subtests for clear failure messages. - Do call
t.Parallel()at both the top-level test function and inside eacht.Runsubtest. - Use
tt(nottc) as the conventional loop variable for test cases in this codebase.
-
testify assertions
- Use
github.com/stretchr/testify/require(notassert) —requirefails immediately on error, preventing cascading failures. - Common assertions:
require.NoError,require.Error,require.Equal,require.EqualValues,require.Len,require.Contains,require.True,require.False,require.Nil,require.NotNil. - Add descriptive messages to assertions when helpful:
require.NoError(t, err, "could not create test store").
- Use
-
test context and cleanup
- Use
t.Context()instead ofcontext.Background()orcontext.TODO()in tests — this provides a context that is automatically cancelled when the test ends. - Use
t.TempDir()for temporary directories — automatically cleaned up after the test. - Use
t.Cleanup(func() { ... })for registering teardown logic instead ofdeferwhen the cleanup must survive subtest boundaries.
- Use
-
mocks (mockery + testify)
- Mocks are auto-generated by mockery into
ee/agent/types/mocks/(and similarmocks/directories). - Interfaces annotated with
//mockery:generate: trueget mock implementations generated. - Do not edit generated mock files — regenerate them with
mockeryif the interface changes. - Instantiate mocks via constructors like
typesmocks.NewKnapsack(t)ortypesmocks.NewFlags(t)— they auto-registert.Cleanupto assert expectations. - Use
.On("MethodName", args...).Return(values...)to set up expectations; use.Maybe()for optional calls.
- Mocks are auto-generated by mockery into
-
test storage helpers (storageci)
- Use
storageci.NewStore(t, slogger, bucketName)fromee/agent/storage/cito create test KV stores. It automatically uses in-memory storage in CI and bbolt locally. - Use
storageci.SetupDB(t)to create a temporary bbolt database for tests — it handlest.TempDir()andt.Cleanup()for you. - Use
storageci.MakeStores(t, slogger, db)to create all standard launcher stores at once for integration tests.
- Use
-
NopLogger for tests
- Always use
multislogger.NewNopLogger()(frompkg/log/multislogger) as the*slog.Loggerin tests, unless you need to assert log output. - If you need to capture log output, create a real slogger writing to a
threadsafebuffer.ThreadSafeBuffer.
- Always use
- This codebase uses the functional options pattern extensively for configurable constructors.
- Define an unexported struct, a public
Optiontype alias (type Option func(*myStruct)), andWithXxxfunctions returningOption. - Apply options with a variadic
opts ...Optionparameter in the constructor.
- Components that need to react to flag/configuration changes implement
types.FlagsChangeObserver. - Register with
flags.RegisterChangeObserver(observer, keys.SomeFlag, keys.AnotherFlag). - In
FlagsChanged, check which keys changed usingslices.Contains(flagKeys, keys.TargetKey)and react accordingly. - This pattern is used throughout the codebase (e.g.,
tablewrapper,TelemetryExporter,Runner).
- Use
observability.StartSpan(ctx, keyVals...)to create traced spans. Always deferspan.End(). - Key-value pairs in
StartSpanfollow the samesnake_casekey convention as slog. - Use
observability.SetError(span, err)to record errors on spans. - Spans auto-extract caller information (file, line, function) for naming.
- All custom osquery tables must be wrapped with
tablewrapper.New(...)for timeout protection and concurrency limiting. - Define a generate function matching
func(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error). - Register columns with
table.ColumnDefinitionand pass everything throughtablewrapper.New. - Use
tablehelpers.MockQueryContext(...)in tests to simulate osquery query contexts.
- Always run
make lintafter making changes to catch style and correctness issues before committing. - The linter configuration lives in
.golangci.ymlat the repo root. - Key linters enabled:
forbidigo: blocks rawgo func(),os.Exit,panic, and directexec.Command.sloglint: enforces kv-only slog messages,snake_casekeys, static messages, and context in all calls.noctx/containedctx: ensures proper context passing (no embedded contexts in structs, no HTTP requests without context).govet,errcheck,staticcheck: standard Go correctness linters.
- To run:
make lint(requiresgolangci-lintinstalled locally).