Guidance for Claude Code when working with this repository.
Atmos: Go CLI for cloud infrastructure orchestration via Terraform/Helmfile/Packer with stack-based config, templating, policy validation, vendoring, and terminal UI.
This repository uses git worktrees for parallel development. When working in a worktree:
- ALWAYS stay within the current working directory - Never escape to parent directories
- Use relative paths or paths under the current working directory - Do not hardcode
/Users/*/atmos/paths - The worktree IS the repository - All files, including
pkg/,cmd/,internal/, exist within the worktree - Never assume the parent directory is the repo - Worktrees like
.conductor/branch-name/are complete, independent working copies
Why this matters: Searching outside the worktree will find stale code from the main branch instead of the current branch's code. This leads to incorrect analysis and recommendations.
For Task agents: When searching for files, always use the current working directory (.) or relative paths. Never construct absolute paths that might escape the worktree.
Multiple Claude sessions may be working on the same branch or worktree simultaneously. To avoid destroying other sessions' work:
- NEVER delete, reset, or discard files you didn't create - Other sessions may have created them
- NEVER run
git reset,git checkout --, orgit cleanwithout explicit user approval - ALWAYS ask the user before removing untracked files - They may be work-in-progress from another session
- When you see unfamiliar files, assume another session created them - ask the user what to do
- If pre-commit hooks fail due to files you didn't touch, ask the user how to proceed rather than trying to fix or remove them
Why this matters: The user may have multiple Claude sessions working in parallel on different aspects of a feature. Deleting “unknown” files destroys that work.
# Build & Test
make build # Build to ./build/atmos
make testacc # Run tests
make testacc-cover # Tests with coverage
make lint # golangci-lint on changed filescmd/- CLI commands (one per file)internal/exec/- Business logicpkg/- config, stack, component, utils, validate, workflow, hooks, telemetry
Stack Pipeline: Load atmos.yaml → process imports/inheritance → apply overrides → render templates → generate config.
Templates and YAML functions: Go templates + Gomplate with atmos.Component(), !terraform.state, !terraform.output, store integration.
Atmos has specialized domain experts in .claude/agents/ for focused subsystems. Use agents instead of inline work for their areas of expertise.
Available Agents:
@agent-developer- Creating/maintaining agents, agent architecture@tui-expert- Terminal UI, theme system, output formatting@atmos-errors- Error handling patterns, error builder usage@flag-handler- CLI commands, flag parsing, CommandProvider pattern@example-creator- Creating examples, mock components, test cases, EmbedFile docs
When to delegate:
- TUI/theme changes →
@tui-expert - New CLI commands →
@flag-handler - Error handling refactoring →
@atmos-errors - Creating new agents →
@agent-developer - Creating examples/demos →
@example-creator
Benefits: Agents are domain experts with deep knowledge of patterns, PRDs, and subsystem architecture. They ensure consistency and best practices.
See .claude/agents/README.md for full list and docs/prd/claude-agent-architecture.md for architecture.
Use registry pattern for extensibility. Existing implementations:
- Command Registry:
cmd/internal/registry.go- All commands register viaCommandProviderinterface - Store Registry:
pkg/store/registry.go- Multi-provider store implementations
New commands MUST use command registry pattern. See docs/prd/command-registry-pattern.md
- Define interfaces for all major functionality. Use dependency injection for testability
- Generate mocks with
go.uber.org/mock/mockgen - Avoid integration tests by mocking external dependencies
Use functional options pattern for configuration instead of functions with many parameters. Provides defaults, extensible without breaking changes.
Use context.Context only for:
- Cancellation signals across API boundaries
- Deadlines/timeouts for operation time limits
- Request-scoped values (trace IDs, request IDs - sparingly)
DO NOT use context for: Passing configuration (use Options pattern), passing dependencies (use struct fields or DI), or avoiding proper function parameters.
Context should be first parameter in functions that accept it.
Atmos separates I/O (streams) from UI (formatting) for clarity and testability.
Two-layer architecture:
- I/O Layer (
pkg/io/) - Stream access (stdout/stderr/stdin), terminal capabilities, masking - UI Layer (
pkg/ui/) - Formatting (colors, styles, markdown rendering)
Output functions:
// Data channel (stdout) - for pipeable output
data.Write/Writef/Writeln("result")
data.WriteJSON/WriteYAML(structData)
// UI channel (stderr) - for human messages
ui.Write/Writef/Writeln("message") // Plain (no icon, no color)
ui.Success/Error/Warning/Info("status") // With icons and colors
ui.Markdown/MarkdownMessage("text") // Formatted docsAnti-patterns (DO NOT use):
fmt.Fprintf(os.Stdout/Stderr, ...) // Use data.* or ui.* instead
fmt.Println(...) // Use data.Writeln() insteadZero-Configuration Degradation: Write code assuming full TTY - system automatically handles color degradation, width adaptation, TTY detection, CI detection, markdown rendering, icon support, secret masking, and format-aware masking.
Force Flags (for screenshot generation):
--force-tty/ATMOS_FORCE_TTY=true- Force TTY mode--force-color/ATMOS_FORCE_COLOR=true- Force TrueColor output
See pkg/io/example_test.go for comprehensive examples.
Atmos uses Gitleaks pattern library (120+ patterns). Disable masking: atmos terraform plan --mask=false
- Avoid utils package bloat - Don't add new functions to
pkg/utils/ - Create purpose-built packages - New functionality gets its own package in
pkg/ - Examples:
pkg/store/,pkg/git/,pkg/pro/,pkg/filesystem/
All comments must end with periods (enforced by godot linter).
NEVER delete existing comments without a very strong reason. Preserve helpful comments explaining why/how/what/where. Update comments to match code when refactoring.
Three groups separated by blank lines, sorted alphabetically:
- Go stdlib
- 3rd-party (NOT cloudposse/atmos)
- Atmos packages
Maintain aliases: cfg, log, u, errUtils
Add defer perf.Track(atmosConfig, "pkg.FuncName")() + blank line to all public functions. Use nil if no atmosConfig param.
Exceptions (do NOT add perf.Track):
- Trivial getters/setters (e.g.,
GetName(),SetValue()) - Command constructor functions (e.g.,
DescribeCommand(),ListCommand()) - Simple factory functions that just return structs
- Functions that only delegate to another tracked function
- Pure validation/lookup functions with no I/O (e.g.,
ValidateCloudEnvironment(),ResolveDestination())
Precedence: CLI flags → ENV vars → config files → defaults (use Viper)
CRITICAL: Unified flag parsing infrastructure is FULLY IMPLEMENTED in pkg/flags/.
- Commands MUST use
flags.NewStandardParser()for command-specific flags - NEVER call
viper.BindEnv()orviper.BindPFlag()directly - Forbidigo enforces this - See
cmd/version/version.gofor reference implementation - Consult flag-handler agent for all flag-related work
- All errors MUST be wrapped using static errors defined in
errors/errors.go - Use
errors.Joinfor combining multiple errors - preserves all error chains - Use
fmt.Errorfwith%wfor adding string context - Use error builder for complex errors - adds hints, context, and exit codes
- Use
errors.Is()for error checking - robust against wrapping - NEVER use dynamic errors directly - triggers linting warnings
- See
docs/errors.mdfor complete developer guide
- Prefer unit tests with mocks over integration tests
- Use interfaces + dependency injection for testability
- Generate mocks with
go.uber.org/mock/mockgen - Table-driven tests for comprehensive coverage
- Target >80% coverage
ALWAYS use cmd.NewTestKit(t) for cmd tests. Auto-cleans RootCmd state (flags, args).
- Test behavior, not implementation
- Never test stub functions - either implement or remove
- Avoid tautological tests
- Make code testable via DI
- No coverage theater
- Remove always-skipped tests
- Use
errors.Is()for error checking - For aliasing/isolation tests, verify BOTH directions: after a merge, mutate the result and confirm the original inputs are unchanged (result→src isolation); also mutate a source map before the merge and confirm the result is unaffected (src→result isolation).
- For slice-result tests, assert element contents, not just length:
require.Lenalone allows regressions that drop or corrupt contents. Assert at least the first and last element by value. - Never use platform-specific binaries in tests (e.g.,
false,true,shon Unix): these don't exist on Windows. Use Go-native test helpers: subprocess viaos.Executable()+TestMain, temp files with cross-platform scripts, or DI to inject a fake command runner. - Safety guards must fail loudly: any check that counts fixture files or validates test preconditions must use
require.Positive(or equivalent) — neverif count > 0 { ... }which silently disables the check when misconfigured. - Use absolute paths for fixture counting: any
filepath.WalkDiror file-count assertion must use an already-resolved absolute path (not a relative one) to be CWD-independent. - Add compile-time sentinels for schema field references in tests: when a test uses a specific struct field (e.g.,
schema.Provider{Kind: "azure"}), addvar _ = schema.Provider{Kind: "azure"}as a compile guard so a field rename immediately fails the build. - Add prerequisite sub-tests for subprocess behavior: when a test depends on implicit env propagation (e.g.,
ComponentEnvListreaching a subprocess), add an explicit sub-test that confirms the behavior before the main test runs. - Contract vs. legacy behavior: if a test says "matches mergo" (or any other library), add an opt-in cross-validation test behind a build tag (e.g.,
//go:build compare_mergo); otherwise state "defined contract" explicitly so it's clear the native implementation owns the behavior. Run cross-validation tests with:go test -tags compare_mergo ./pkg/merge/... -run CompareMergo -v(requires mergo v1.0.x installed). - Include negative-path tests for recovery logic: whenever a test verifies that a recovery/fallback triggers under condition X, add a corresponding test that verifies the recovery does NOT trigger when condition X is absent (e.g., mismatched workspace name).
When a PR defers work to a follow-up (e.g., migration, cleanup, refactor), open a GitHub issue and link it by number in the blog post, roadmap, and/or PR description before merging. Blog posts with "a follow-up issue will..." with no #number are incomplete — the work will never be tracked.
Use go.uber.org/mock/mockgen with //go:generate directives. Never manual mocks.
Embed examples from cmd/markdown/*_usage.md using //go:embed. Render with utils.PrintfMarkdown().
Small focused files (<600 lines). One cmd/impl per file. Co-locate tests. Never //revive:disable:file-length-limit.
Preconditions: Tests skip gracefully with helpers from tests/test_preconditions.go. See docs/prd/testing-strategy.md.
Commands: make test-short (quick), make testacc (all), make testacc-cover (coverage)
Fixtures: tests/test-cases/ for integration tests
Golden Snapshots (MANDATORY):
- NEVER manually edit golden snapshot files - Always use
-regenerate-snapshotsflag - Snapshots capture exact output including invisible formatting (lipgloss padding, ANSI codes, trailing whitespace)
- Different environments produce different output (terminal width, Unicode support, styling libraries)
Regeneration:
go test ./tests -run 'TestCLICommands/test_name' -regenerate-snapshots
git diff tests/snapshots/CRITICAL: Never use pipe redirection when running tests. Piping breaks TTY detection.
Golden Snapshot Files:
- NEVER modify files under
tests/test-cases/ortests/testdata/unless explicitly instructed - These contain golden snapshots sensitive to even minor changes
- Create
cmd/[command]/with CommandProvider interface - Add blank import to
cmd/root.go - Implement in
internal/exec/mycommand.go - Add tests, Docusaurus docs in
website/docs/cli/commands/ - Build website:
cd website && npm run build
See docs/developing-atmos-commands.md and docs/prd/command-registry-pattern.md
All cmds/flags need Docusaurus docs in website/docs/cli/commands/. Use <dl> for args/flags. Build: cd website && npm run build
Verifying Links: Find doc file (find website/docs/cli/commands -name "*keyword*"), check slug in frontmatter (head -10 <file> | grep slug), verify existing links (grep -r "<url>" website/docs/).
Common mistakes: Using command name vs. filename, not checking slug frontmatter, guessing URLs.
CLI command docs MUST include:
- Frontmatter - title, sidebar_label, sidebar_class_name, id, description
- Intro component -
import Intro from '@site/src/components/Intro'then<Intro>Brief description</Intro> - Screengrab -
import Screengrab from '@site/src/components/Screengrab'then<Screengrab title="..." slug="..." /> - Usage section - Shell code block with command syntax
- Arguments/Flags - Use
<dl><dt>for each argument/flag with<dd>description - Examples section - Practical usage examples
File location: website/docs/cli/commands/<command>/<subcommand>.mdx
ALWAYS build after doc changes: cd website && npm run build. Verify: no broken links, missing images, MDX component rendering.
When: After modifying CLI behavior/help/output, adding commands. NOT for doc-only changes.
How (Linux/CI only):
- GitHub Actions:
gh workflow run screengrabs.yaml(creates PR) - Local Linux:
cd demo/screengrabs && make all - Docker (macOS):
make -C demo/screengrabs docker-all
Notes: Captures exact output, ANSI→HTML, script syntax differs BSD/GNU, regenerate all together, no pipe indirection.
All Product Requirement Documents (PRDs) MUST be placed in docs/prd/. Use kebab-case filenames.
Follow template (what/why/references).
Blog Posts (CI Enforced):
- PRs labeled
minor/majorMUST include blog post:website/blog/YYYY-MM-DD-feature-name.mdx - Use
.mdxwith YAML front matter,<!--truncate-->after intro - MUST read
website/blog/tags.yml- Only use tags defined there, never invent new tags - MUST read
website/blog/authors.yml- Use existing author or add new entry for committer
Blog Template:
---
slug: descriptive-slug
title: "Clear Title"
authors: [username]
tags: [feature]
---
Brief intro.
<!--truncate-->
## What Changed / Why This Matters / How to Use It / Get InvolvedValid Tags (from website/blog/tags.yml):
- User-facing:
feature,enhancement,bugfix,dx,breaking-change,security,documentation,deprecation - Internal:
core(for contributor-only changes with zero user impact)
Roadmap Updates (CI Enforced):
- PRs labeled
minor/majorMUST also updatewebsite/src/data/roadmap.js - For new features: Add milestone to relevant initiative with
status: 'shipped' - Link to changelog: Add
changelog: 'your-blog-slug'to the milestone - Link to PR: Add
pr: <pr-number>to the milestone - Update initiative
progresspercentage:(shipped milestones / total milestones) * 100 - See
.claude/agents/roadmap.mdfor detailed update instructions
Use no-release label for docs-only changes.
Check status: gh pr checks {pr} --repo cloudposse/atmos
Reply to threads: Use gh api graphql with addPullRequestReviewThreadReply
- All new commands/flags/parameters MUST have Docusaurus documentation
- Use definition lists
<dl>instead of tables for arguments and flags - Follow Docusaurus conventions from existing files
- File location:
website/docs/cli/commands/<command>/<subcommand>.mdx - Link to documentation using current URL paths (e.g.,
/stacks,/components,/cli/configuration) - Include purpose note and help screengrab
- Use consistent section ordering: Usage → Examples → Arguments → Flags
ALWAYS build the website after documentation changes: cd website && npm run build
- Write a test to reproduce the bug
- Run the test to confirm it fails
- Fix the bug iteratively
- Verify fix doesn't break existing functionality
Don't commit: todos, research, scratch files. Do commit: code, tests, requested docs, schemas. Update .gitignore for patterns only.
NEVER run destructive git commands without explicit user confirmation:
git reset HEADorgit reset --hard- discards staged/committed changesgit checkout HEAD -- .orgit checkout -- .- discards all working changesgit clean -fd- deletes untracked filesgit stash drop- permanently deletes stashed changes
Always ask first: "This will discard uncommitted changes. Proceed? [y/N]"
80% minimum (CodeCov enforced). All features need tests. make testacc-coverage for reports.
golangci-lint enforces cyclop: max-complexity: 15 and funlen: lines: 60, statements: 40.
When refactoring high-complexity functions:
- Extract blocks with clear single responsibilities into named helper functions.
- Use the pattern:
buildXSubcommandArgs,resolveX,checkX,assembleX,handleX. - Keep the orchestrator function as a flat linear pipeline of named steps (see
ExecuteTerraform). - Previously high-complexity functions:
ExecuteTerraform(160→26, seeinternal/exec/terraform.go),ExecuteDescribeStacks(247→10),processArgsAndFlags.
Use viper.BindEnv("ATMOS_VAR", "ATMOS_VAR", "FALLBACK") - ATMOS_ prefix required.
UI (prompts, status) → stderr. Data → stdout. Logging for system events only. Never use logging for UI.
Update all schemas in pkg/datafetcher/schema/ when adding config options.
Use colors from pkg/ui/theme/colors.go
New configs support Go templating with FuncMap() from internal/exec/template_funcs.go
Search internal/exec/ and pkg/ before implementing. Extend, don't duplicate.
Linux/macOS/Windows compatible. Use SDKs over binaries. Use filepath.Join() instead of hardcoded path separators.
Subprocess helpers in tests (cross-platform):
Instead of exec.LookPath("false") or other Unix-only binaries, use the test binary itself.
Important: If your package already has a TestMain, add the env-gate check inside the existing TestMain — do not add a second TestMain function (Go does not allow two in the same package).
// In testmain_test.go — merge this check into the existing TestMain:
func TestMain(m *testing.M) {
// If _ATMOS_TEST_EXIT_ONE is set, exit immediately with code 1.
// This lets tests use the test binary itself as a cross-platform "exit 1" command.
if os.Getenv("_ATMOS_TEST_EXIT_ONE") == "1" { os.Exit(1) }
os.Exit(m.Run())
}
// NOTE: If your package already defines TestMain, insert the _ATMOS_TEST_EXIT_ONE
// check at the top of the existing function rather than copying the whole snippet.
// In the test itself:
exePath, _ := os.Executable()
info.Command = exePath
info.ComponentEnvList = []string{"_ATMOS_TEST_EXIT_ONE=1"}Path handling in tests:
- NEVER use forward slash concatenation like
tempDir + "/components/terraform/vpc" - ALWAYS use
filepath.Join()with separate arguments:filepath.Join(tempDir, "components", "terraform", "vpc") - NEVER use forward slashes in
filepath.Join()likefilepath.Join(dir, "a/b/c")- usefilepath.Join(dir, "a", "b", "c") - NEVER hardcode Unix paths in expected values like
assert.Equal(t, "/project/components/vpc", path)- build expected paths withfilepath.Join() - For path suffix checks, use
filepath.ToSlash()to normalize:strings.HasSuffix(filepath.ToSlash(path), "expected/suffix") - NEVER use bash/shell commands in tests - use Go stdlib (
os,filepath,io) for file operations
Why: Windows uses backslash (\) as path separator, Unix uses forward slash (/). Hardcoded paths fail on Windows CI.
Follow registry pattern: define interface, implement per provider, register implementations, generate mocks. Example: pkg/store/
Auto-enabled via RootCmd.ExecuteC(). Non-standard paths use telemetry.CaptureCmd(). Never capture user data.
Prerequisites: Go 1.26+, golangci-lint, Make. See .cursor/rules/atmos-rules.mdc.
Build: CGO disabled, cross-platform, version via ldflags, output to ./build/
ALWAYS compile after changes: go build . && go test ./.... Fix errors immediately.
NEVER use --no-verify. Run make lint before committing. Hooks run go-fumpt, golangci-lint, go mod tidy.