diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index baabffad35..1f246fb171 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,26 +1,120 @@ -## Running Tests +# CLAUDE.md +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## About Fleet + +Fleet is an open-source platform for IT and security teams: device management (MDM), vulnerability reporting, osquery fleet management, and security monitoring. Go backend, React/TypeScript frontend, manages thousands of devices across macOS, Windows, Linux, iOS, iPadOS, Android, and ChromeOS. + +## Architecture + +### Backend request flow +HTTP request → `server/service/handler.go` routes → endpoint function (decode request) → service method (auth + business logic) → datastore method (SQL) → response struct + +### Key layers +- **Types & interfaces**: `server/fleet/` — `Service` in `service.go`, `Datastore` in `datastore.go` +- **Service implementations**: `server/service/` — business logic, auth checks +- **Datastore (MySQL)**: `server/datastore/mysql/` — SQL queries, migrations +- **Enterprise features**: `ee/server/service/` — wraps core service with license checks +- **MDM**: `server/mdm/` — Apple, Microsoft, Android device management +- **Frontend**: `frontend/pages/` (routes), `frontend/components/` (reusable UI), `frontend/services/` (API client) +- **CLI tools**: `cmd/fleet/` (server), `cmd/fleetctl/` (management CLI), `orbit/` (agent) + +### Enterprise vs core +- Core features: no special build tags, available in all deployments +- Enterprise features: in `ee/` directory, license checks at service layer +- Use `//go:build !premium` for core-only features when needed + +## Terminology + +The following terms were recently renamed. Use the new terms in conversation and new code, but don't rename existing variables or API parameters without guidance: +- **"Teams" → "Fleets"** — the concept of grouping hosts. Legacy code still uses `team_id`, `teams` table, etc. +- **"Queries" → "Reports"** — what was formerly a "query" in the product is now a "report." The word "query" now refers solely to a SQL query, which is one aspect of a report. + +## Fleet-specific patterns + +### Go backend +- **Error wrapping**: `ctxerr.Wrap(ctx, err, "description")` — never pkg/errors +- **Request/Response**: lowercase struct types, `Err error` field, `Error()` method returning `r.Err` +- **Endpoint registration**: `ue.POST("/api/_version_/fleet/resource", fn, reqType{})` +- **Authorization**: `svc.authz.Authorize(ctx, entity, fleet.ActionX)` at start of service methods +- **Logging**: slog with `DebugContext/InfoContext/WarnContext/ErrorContext` — never bare slog.Debug/Info/Warn/Error +- **Pointers**: Use Go 1.26 `new(expression)` for pointer values (e.g., `new("value")`, `new(true)`, `new(42)`). Do NOT use the legacy `server/ptr` package in new code — it exists throughout the codebase but is superseded by `new(expr)`. +- **Reference example**: `server/service/vulnerabilities.go` + +## Before writing a fix + +- Identify WHERE in the request lifecycle the problem manifests (creation vs team-addition vs sync vs query). Fix it there, not at the reproduction step. +- Read the surrounding 100 lines. If similar checks exist nearby, follow their pattern exactly. +- If an endpoint has zero DB interaction, that's intentional. Adding DB calls needs justification. +- Cover ALL entry points for the same operation (single add, batch/GitOps, etc.). +- For declarative/batch endpoints, validate within the incoming payload, not against the DB. +- When checking for duplicates, exclude the current entity to avoid false conflicts on upserts. +- Run `go test ./server/service/` after adding new datastore interface methods — uninitialized mocks crash other tests. + +## Development commands + +Check the `Makefile` for the full list of available targets. Key ones below. + +### Building and running ```bash -# Quick Go tests (no external deps) -go test ./server/fleet/... - -# Integration tests -MYSQL_TEST=1 go test ./server/datastore/mysql/... -MYSQL_TEST=1 REDIS_TEST=1 go test ./server/service/... - -# Run a specific test -MYSQL_TEST=1 go test -run TestFunctionName ./server/datastore/mysql/... - -# Generate boilerplate for a new frontend component, including associated stylesheet, tests, and storybook -./frontend/components/generate -n RequiredPascalCaseNameOfTheComponent -p optional/path/to/desired/parent/directory +make build # Build fleet + fleetctl +make serve # Start dev server (or: make up) +make generate-dev # Webpack watch mode for frontend dev +make deps # Install dependencies ``` -## Go code style +### Testing +```bash +go test ./server/fleet/... # Quick (no external deps) +MYSQL_TEST=1 go test ./server/datastore/mysql/... # MySQL integration +MYSQL_TEST=1 REDIS_TEST=1 go test ./server/service/... # Service integration +MYSQL_TEST=1 go test -run TestFunctionName ./server/datastore/mysql/... # Specific test +yarn test # Frontend Jest tests +``` -- Prefer `map[T]struct{}` over `map[T]bool` when the map represents a set. -- Convert a map's keys to a slice with `slices.Collect(maps.Keys(m))` instead of manually appending in a loop. -- Avoid `time.Sleep` in tests. Prefer `testing/synctest` to run code in a fake-clock bubble, or use polling helpers, channels, or `require.Eventually`. -- Use `require` and `assert` from `github.com/stretchr/testify` in tests. -- Use `t.Context()` in tests instead of `context.Background()`. -- Use `any` instead of `interface{}` -- Use `math/rand/v2` instead of `math/rand`. +### Linting +```bash +make lint-go-incremental # Go — ONLY changes since branching from main (use after editing) +make lint-go # Go — full (use before committing) +make lint-js # JS/TS linters +``` + +### Database +```bash +make migration name=CamelCaseName # Create new migration +make db-reset # Reset dev database +``` + +### CI test bundles +| Bundle | Packages | Env vars | +|--------|----------|----------| +| `fast` | No external deps | none | +| `mysql` | `server/datastore/mysql/...` | `MYSQL_TEST=1` | +| `service` | `server/service/` (unit) | `MYSQL_TEST=1 REDIS_TEST=1` | +| `integration-core` | `server/service/integration_*_test.go` | `MYSQL_TEST=1 REDIS_TEST=1` | +| `integration-enterprise` | `ee/server/service/integration_*_test.go` | `MYSQL_TEST=1 REDIS_TEST=1` | +| `integration-mdm` | MDM integration tests | `MYSQL_TEST=1 REDIS_TEST=1` | +| `fleetctl` | `cmd/fleetctl/...` | varies | +| `vuln` | `server/vulnerabilities/...` | varies | +| `main` | Everything else | varies | + +## Skills and agents + +Type `/` to see available skills. Key ones: `/test`, `/lint`, `/review-pr`, `/fix-ci`, `/spec-story`, `/new-endpoint`, `/new-migration`, `/bump-migration`, `/project`, `/fleet-gitops`, `/find-related-tests`. + +Agents: **go-reviewer** (proactive after Go edits), **frontend-reviewer** (proactive after TS edits), **fleet-security-auditor** (on-demand for auth/MDM/security). + +## Documentation + +All Fleet documentation lives in this repo. Check these sources before searching the web: + +- **`docs/`** — User-facing docs: feature guides, REST API reference, configuration, deployment, contributing +- **`handbook/`** — Internal procedures: engineering practices, company policies, product design +- **`articles/`** — Blog posts and tutorials + +## Other references + +- Linter config: `.golangci.yml` +- Activity types: `docs/Contributing/reference/audit-logs.md` +- Claude Code setup: `.claude/README.md` diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 0000000000..3674d54c8a --- /dev/null +++ b/.claude/README.md @@ -0,0 +1,480 @@ +# Fleet Claude Code configuration + +This directory contains team-shared [Claude Code](https://claude.ai/code) configuration for the Fleet project. Everything here works out of the box with no MCP servers, plugins, or external dependencies required. The full setup adds ~2,500 tokens at startup — rules, skill bodies, and agent bodies only load on demand. + +This setup is a starting point. You can customize it by creating `.claude/settings.local.json` (gitignored) to add your own permissions, MCP servers, and plugins. See [Customize your setup](#customize-your-setup) for details. + +If you're new to Claude Code, start with the [primer](#claude-code-primer) below. If you already know Claude Code, skip to [what's here](#whats-here). + +### Try it on your branch + +To test this setup without switching branches, pull the `.claude/` folder into your current working branch: + +```bash +# Add the configuration to your branch +git checkout origin/cc-setup-teamwide -- .claude/ + +# Start a Claude Code session and work normally (use --debug to see hooks firing) +claude --debug + +# When you're done testing, fully remove it so nothing ends up in your PR +git checkout -- .claude/ +git clean -fd .claude/ +``` + +This drops the full setup (rules, skills, agents, hooks, and permissions) into your working tree. Start a new Claude Code session and everything loads automatically. When you're done, the second command reverts `.claude/` to whatever's on your branch. + +To troubleshoot hooks or see exactly what's firing, start with `claude --debug`. Check the debug log at `~/.claude/debug/` for detailed hook and tool execution traces. + +### Not covered by this configuration + +The following areas have their own conventions and aren't covered by the current rules, hooks, or skills: + +- **`website/`** — Fleet marketing website (Sails.js, separate `package.json` and conventions) +- **`ee/fleetd-chrome/`** — Chrome extension for ChromeOS (TypeScript, separate test setup) +- **`ee/vulnerability-dashboard/`** — Vulnerability dashboard (Sails.js/Grunt, legacy patterns) +- **`android/`** — Android app (Kotlin/Gradle, separate build system) +- **`third_party/`** — Forked external code (not Fleet's conventions) +- **Documentation** — Guides, API docs, and handbook documentation workflows +- **Fleet-maintained apps (FMA)** — FMA catalog workflows, maintained-app packaging, and `ee/maintained-apps/` conventions +- **MDM-specific patterns** — `server/mdm/` has complex multi-platform patterns (Apple, Windows, Android) beyond what the Go backend rule covers + +--- + +## Claude Code primer + +Claude Code is an AI coding assistant that runs in your terminal, VS Code, JetBrains, desktop app, or browser. It reads your codebase, writes code, runs commands, and understands project context through configuration files like the ones in this directory. + +### Core concepts + +**CLAUDE.md** — Project instructions loaded at session start, like a `.editorconfig` for AI. Claude reads these automatically to understand your project's conventions, architecture, and workflows. There can be multiple: root-level, `.claude/CLAUDE.md`, and user-level `~/.claude/CLAUDE.md`. + +**Skills** — Reusable workflows invoked with `/` (e.g., `/test`, `/fix-ci`). Each skill is a `SKILL.md` file with YAML frontmatter that controls when it triggers, which tools it can use, and whether it runs in an isolated context. Skills replace the older `.claude/commands/` format, adding auto-invocation, tool restrictions, and isolated execution. + +**Agents (subagents)** — Specialized AI assistants that run in isolated contexts with their own tools and model. Claude can delegate to them automatically (if their description includes "PROACTIVELY") or you can invoke them by name. + +**Rules** — Coding conventions that auto-apply based on file paths. When you edit a `.go` file, Go rules load automatically. When you edit `.tsx`, frontend rules load. + +**Hooks** — Shell scripts that run automatically on events like editing files (`PostToolUse`) or before running a tool (`PreToolUse`). Our hooks auto-format Go and TypeScript files on every edit. + +**MCP servers** — External tool integrations via the Model Context Protocol. Connect Claude to GitHub, databases, documentation search, and other services. These aren't required for the team setup but can enhance your personal workflow. + +**Plugins** — Bundled packages of skills, agents, hooks, and MCP configs from the Claude Code marketplace. Like MCP servers, these are optional personal enhancements. + +**Memory** — Claude maintains auto-generated memory across sessions at `~/.claude/projects//memory/`. It remembers patterns, preferences, and lessons learned. View with `/memory`. + +### Commands, shortcuts, and session management + +**Sessions** + +| Action | How | +|--------|-----| +| Start a session | `claude` (terminal) or open in IDE | +| Continue last session | `claude -c` or `/resume` | +| Resume a named session | `claude -r "name"` or `/resume` | +| Rename session | `/rename ` | +| Branch conversation | `/branch` (explore alternatives in parallel) | +| Rewind to checkpoint | `Esc` twice, or `/rewind` | +| Export session | `/export` | +| Side question | `/btw ` (doesn't affect conversation history) | + +**Context** — The context window fills over time. Manage it actively: + +| Action | How | +|--------|-----| +| Check context usage | `/context` | +| Compress conversation | `/compact` or `/compact ` (e.g., `/compact keep the migration plan, drop debugging`) | +| Clear and start fresh | `/clear` | + +Use `/clear` between unrelated tasks — context pollution degrades quality. Use `/compact` when context gets large. Delegate heavy investigation to subagents to keep the main context clean. Press `Esc` twice to rewind if Claude goes off track. + +**Configuration and diagnostics** + +| Action | How | +|--------|-----| +| Invoke a skill | Type `/` then select from menu | +| Switch model | `/model` (sonnet/opus/haiku) | +| Set effort level | `/effort` (low/medium/high) | +| Toggle extended thinking | `Option+T` (macOS) / `Alt+T` | +| Cycle permission mode | `Shift+Tab` | +| Enter plan mode | `/plan ` or `Shift+Tab` | +| Edit plan externally | `Ctrl+G` | +| Manage permissions | `/permissions` or `/allowed-tools` | +| Open settings | `/config` | +| View diff of changes | `/diff` | +| Check session cost | `/cost` | +| Check version and status | `/status` | +| Run installation health check | `/doctor` | +| List all commands | `/help` | + +### Advanced features + +**Plan mode** — Separates research from implementation. Claude explores the codebase and writes a plan for your review before making changes. Activate with `Shift+Tab`, `/plan`, or `--permission-mode plan`. Edit the plan externally with `Ctrl+G`. + +**Extended thinking** — Gives Claude more reasoning time for complex problems. Toggle with `Option+T` (macOS) / `Alt+T`. Set effort level with `/effort`. Include "ultrathink" in prompts for maximum depth. + +**Auto mode** — Uses a background safety classifier to auto-approve safe tool calls without prompting. Cycle to it with `Shift+Tab`. Configure trusted domains and environments in `settings.json` under `autoMode`. + +**Permission modes** — A spectrum from restrictive to autonomous: +- `default` — Reads freely, prompts for writes and commands +- `acceptEdits` — Auto-approves file edits, prompts for commands +- `plan` — Read-only exploration +- `auto` — Classifier-based decisions +- `dontAsk` — Auto-denies tools unless pre-approved via `/permissions` or settings +- `bypassPermissions` — No checks (CI/CD use only) + +**Headless and CI mode** — Run non-interactively with `claude -p "prompt" --output-format json`. Useful for CI pipelines, batch processing, and scripted workflows. + +**Background tasks** — Long-running work continues while you chat. Skills with `context: fork` run in isolated subagents. + +**Git worktrees** — Run `claude --worktree` to work in an isolated git worktree so experimental changes don't affect your working directory. + +### Settings hierarchy + +Settings are applied in this order (highest to lowest priority): + +1. **Managed** — Organization-wide policies (IT/admin controlled) +2. **Local** — `.claude/settings.local.json` (personal, gitignored) +3. **Project** — `.claude/settings.json` (team-shared, checked in) +4. **User** — `~/.claude/settings.json` (personal, all projects) + +Your local settings override project settings, so you can always customize without affecting the team. + +--- + +## What's here + +``` +.claude/ +├── CLAUDE.md # Project instructions (architecture, patterns, commands) +├── settings.json # Team settings (env vars, permissions, hooks) +├── settings.local.json # Personal overrides (gitignored) +├── README.md # This file +├── rules/ # Path-scoped coding conventions (auto-applied) +│ ├── fleet-go-backend.md # Go: ctxerr, service patterns, logging, testing +│ ├── fleet-frontend.md # React/TS: components, React Query, BEM, interfaces +│ ├── fleet-database.md # MySQL: migrations, goqu, reader/writer +│ ├── fleet-api.md # API: endpoint registration, versioning, error responses +│ └── fleet-orbit.md # Orbit: agent packaging, TUF updates, platform-specific code +├── skills/ # Workflow skills (invoke with /) +│ ├── review-pr/ # /review-pr +│ ├── fix-ci/ # /fix-ci +│ ├── test/ # /test [filter] +│ ├── find-related-tests/ # /find-related-tests +│ ├── lint/ # /lint [go|frontend] +│ ├── fleet-gitops/ # /fleet-gitops +│ ├── project/ # /project +│ ├── new-endpoint/ # /new-endpoint +│ ├── new-migration/ # /new-migration +│ ├── bump-migration/ # /bump-migration +│ └── spec-story/ # /spec-story +├── agents/ # Specialized AI agents +│ ├── go-reviewer.md # Go reviewer (proactive, sonnet) +│ ├── frontend-reviewer.md # Frontend reviewer (proactive, sonnet) +│ └── fleet-security-auditor.md # Security auditor (on-demand, opus) +└── hooks/ # Automated hooks + ├── guard-dangerous-commands.sh # PreToolUse: blocks dangerous commands + ├── goimports.sh # PostToolUse: formats Go files + ├── prettier-frontend.sh # PostToolUse: formats frontend files + └── lint-on-save.sh # PostToolUse: lints Go/TS and feeds violations back to Claude +``` + +## Skills reference + +Several skills use the `gh` CLI for GitHub operations (PR review, CI diagnosis, issue speccing). Make sure you have [`gh`](https://cli.github.com/) installed and authenticated with `gh auth login`. + +| Skill | Usage | What it does | +|-------|-------|-------------| +| `/review-pr` | `/review-pr 12345` | Reviews a PR for correctness, Go idioms, SQL safety, test coverage, and Fleet conventions. Runs in isolated context. Requires `gh`. | +| `/fix-ci` | `/fix-ci https://github.com/.../runs/123` | Diagnoses CI failures in 8 steps: identifies failing suites, fetches logs, classifies failures as stale assertions vs real bugs, fixes stale assertions, and reports real bugs. Requires `gh`. | +| `/test` | `/test` or `/test TestFoo` | Detects which packages changed via `git diff` and runs their tests with the correct env vars (`MYSQL_TEST`, `REDIS_TEST`). | +| `/find-related-tests` | `/find-related-tests` | Maps changed files to their `_test.go` files, integration tests, and test helpers. Outputs exact `go test` commands. | +| `/fleet-gitops` | `/fleet-gitops` | Validates GitOps YAML: osquery queries against Fleet schema, Apple/Windows/Android profiles against upstream references, and software against the Fleet-maintained app catalog. | +| `/project` | `/project android-mdm` | Loads or creates a workstream context file in your Claude memory directory. Includes a minimal self-improvement mechanism — Claude adds discoveries, gotchas, and key file paths as you work, so each session starts with slightly richer context than the last. | +| `/new-endpoint` | `/new-endpoint` | Scaffolds a Fleet API endpoint: request/response structs, endpoint function, service method, datastore interface, handler registration, and test stubs. | +| `/new-migration` | `/new-migration` | Creates a timestamped migration file and test file with proper naming, init registration, and Up function (Down is always a no-op). | +| `/bump-migration` | `/bump-migration YYYYMMDDHHMMSS_Name.go` | Bumps a migration's timestamp to current time when it conflicts with a migration already merged to main. Renames files and updates function names in both migration and test files. | +| `/spec-story` | `/spec-story 12345` | Breaks down a GitHub story into implementable sub-issues: maps codebase impact, decomposes into atomic tasks per layer (migration/datastore/service/API/frontend), and writes specs with acceptance criteria and a dependency graph. Requires `gh`. | +| `/lint` | `/lint` or `/lint go` | Runs the appropriate linters (golangci-lint, eslint, prettier) on recently changed files. Accepts `go`, `frontend`, or a file path to narrow scope. | + +### Using `/project` for workstream context + +The `/project` skill builds a personal knowledge base for areas of the codebase you work in repeatedly. Use it at the start of a session to load context from previous sessions. + +**First use:** `/project software` — no file exists yet, so Claude asks you to describe the workstream, explores the codebase, and creates a context file with key files, patterns, and architecture notes. + +**Subsequent sessions:** `/project software` — Claude loads what it knows, summarizes it, and asks what you're working on today. + +**As you work:** Claude adds useful discoveries to the project file — gotchas, important file paths, architectural decisions — so the next session starts with richer context. + +**Organizing projects:** The name is just a label. Pick the scope that's most useful to you: + +| Scope | Example | Good for | +|-------|---------|----------| +| By team area | `/project software`, `/project mdm` | Broad context that accumulates over time. Good if you consistently work in one area. | +| By feature | `/project patch-policies`, `/project android-enrollment` | Focused context for multi-week features. Tracks specific decisions, status, and key files. | +| By issue | `/project 35666-gitops-exceptions` | Narrow, disposable context tied to a specific piece of work. | + +Project files are stored per-machine in your Claude memory directory (`~/.claude/projects/`). They're personal — not shared with the team. Context grows gradually (a few lines per session) and Claude auto-truncates at 200 lines / 25KB, so it won't run away. + +## Agents reference + +### go-reviewer (sonnet, proactive) +Runs automatically after Go file changes. Checks: +- Error handling (ctxerr wrapping, no swallowed errors) +- Database patterns (parameterized queries, reader/writer, and index coverage) +- API conventions (auth checks, response types, and HTTP status codes) +- Test coverage (integration tests for DB code, edge cases) +- Logging (structured slog, no print statements) + +### frontend-reviewer (sonnet, proactive) +Runs automatically after TypeScript and React file changes. Checks: +- TypeScript strictness (no `any`, proper type narrowing) +- React Query patterns (query keys, `enabled` option) +- Component structure (4-file pattern, BEM naming) +- Interface consistency (`I` prefix, `frontend/interfaces/` types) +- Accessibility (ARIA attributes, keyboard navigation) + +### fleet-security-auditor (opus, on-demand) +Invoke when touching auth, MDM, enrollment, or user data. Uses Opus for deeper adversarial reasoning. Checks: +- API authorization gaps (missing `svc.authz.Authorize` calls) +- MDM profile payload injection +- osquery query injection +- Team permission boundary violations +- Certificate and SCEP handling +- PII in logs, license enforcement bypass + +You can add your own agents by creating files in `.claude/agents/` on a branch, or in `~/.claude/agents/` for personal agents that apply across all projects. + +## Hooks + +Four hooks run automatically: + +| Hook | Event | Files | What it does | +|------|-------|-------|-------------| +| `guard-dangerous-commands.sh` | PreToolUse (Bash) | All commands | Blocks `rm -rf /`, force push to main/master, `git reset --hard origin/`, and pipe-to-shell attacks | +| `goimports.sh` | PostToolUse (Edit/Write) | `**/*.go` | Formats with `goimports` → `gofumpt` → `gofmt` (first available) | +| `prettier-frontend.sh` | PostToolUse (Edit/Write) | `frontend/**` | Formats with `npx prettier --write` | +| `lint-on-save.sh` | PostToolUse (Edit/Write) | `**/*.go`, `**/*.ts`, `**/*.tsx` | Auto-fixes with `golangci-lint --fix`, then runs `make lint-go-incremental` (only changes since branching from main) and feeds remaining violations back to Claude for self-correction. For TypeScript, runs `eslint --fix` then reports remaining issues. | + +Hooks run in order: formatters first (goimports, prettier), then the linter. The linter is non-blocking — it doesn't reject the edit, but Claude sees the output and fixes violations in its next step. All hooks exit gracefully if the tool isn't installed. To add project-level hooks, edit `.claude/settings.json` on a branch. For personal hooks, add them to `~/.claude/settings.json`. + +## Rules + +Rules auto-apply when you edit files matching their path globs: + +| Rule | Paths | Key conventions | +|------|-------|----------------| +| `fleet-go-backend.md` | `server/**/*.go`, `cmd/**/*.go`, `orbit/**/*.go`, `ee/**/*.go`, `pkg/**/*.go`, `tools/**/*.go`, `client/**/*.go`, `test/**/*.go` | ctxerr errors, error types, banned imports, input validation, viewer context, auth pattern, `fleethttp.NewClient()`, `new(expression)` pointers, bounded contexts, and service signatures | +| `fleet-frontend.md` | `frontend/**/*.ts`, `frontend/**/*.tsx` | React Query, component structure, BEM/SCSS, permissions utilities, team context (fleets/reports terminology), notifications, XSS prevention, and string/URL utilities | +| `fleet-database.md` | `server/datastore/**/*.go` | Migration naming and testing, goqu queries, reader/writer, transaction rules (no ds.reader/writer inside tx), parameterized SQL, and batch operations | +| `fleet-api.md` | `server/service/**/*.go` | Endpoint registration, API versioning, and error-in-response pattern | +| `fleet-orbit.md` | `orbit/**/*.go` | Agent architecture, TUF updates, platform-specific code, packaging, keystore, and security considerations | + +## Permissions + +`settings.json` pre-approves safe operations so you don't get prompted: + +**Allowed:** `go test`, `go vet`, `go build`, `golangci-lint`, `yarn test/lint`, `npx prettier/eslint/tsc/jest`, `make test/lint/build/generate/serve/db-*/migration/deps/e2e-*`, `git status/diff/log/show/branch`, and `gh pr/issue/run/api` + +**Denied:** `git push --force`, `git push -f`, `rm -rf /`, and `rm -rf ~` + +Commands not in either list (like `git commit` or `git push`) will prompt for permission on first use. To pre-approve them, add them to your `.claude/settings.local.json` — see [local settings](#local-settings) below. + +## Customize your setup + +Everything above works without extra configuration. The sections below describe how to customize your personal experience without affecting the team. + +### Model and effort + +Change the model or effort level for your current session at any time: + +``` +/model opus # Switch to Opus for deeper reasoning +/model sonnet # Switch to Sonnet for faster responses +/effort high # More reasoning time +/effort low # Faster, lighter responses +``` + +Each skill in this setup has an `effort` level tuned for its complexity (e.g., `/spec-story` uses high, `/test` uses low). The skill's effort overrides your session setting while the skill is active, then reverts when it finishes. + +To set your default for all sessions, add to `~/.claude/settings.json`: +```json +{ + "model": "opus[1m]", + "effortLevel": "high" +} +``` + +### Override a shared skill + +Each skill has `effort` and optionally `model` set in its frontmatter. You can't override a specific skill's frontmatter from settings — but you can override the entire skill by creating a personal copy with the same name at a higher-priority location. + +Personal skills (`~/.claude/skills/`) take precedence over project skills (`.claude/skills/`). To override `/test` with a different effort level: + +```bash +# Copy the shared skill to your personal config +mkdir -p ~/.claude/skills/test +cp .claude/skills/test/SKILL.md ~/.claude/skills/test/SKILL.md + +# Edit the frontmatter to change effort, model, or anything else +``` + +Your personal version takes priority. The shared version is ignored for you but still works for everyone else. + +### Override a shared agent + +Same pattern as skills. Personal agents (`~/.claude/agents/`) take precedence over project agents (`.claude/agents/`): + +```bash +# Override go-reviewer with your own version +cp .claude/agents/go-reviewer.md ~/.claude/agents/go-reviewer.md +# Edit to change model, tools, or review criteria +``` + +### Local settings + +Create `.claude/settings.local.json` (gitignored) for personal permission overrides. Local settings take priority over project settings in `.claude/settings.json`. + +Common things to add: +- Git write permissions (the shared setup only allows read operations) +- MCP server tool permissions +- Additional `make` or `bash` commands specific to your workflow +- Additional hooks + +```json +{ + "permissions": { + "allow": [ + "Bash(git add*)", + "Bash(git commit*)", + "Bash(git push)", + "mcp__github__*", + "mcp__my-mcp-server__*" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "my-personal-hook.sh", + "timeout": 10 + } + ] + } + ] + } +} +``` + +Local hooks run in addition to shared hooks, not instead of them. Permission rules merge across levels, with deny taking precedence: if the shared settings deny something, local settings can't override it. + +### Personal CLAUDE.md + +Create a root-level `CLAUDE.md` (gitignored) for personal instructions that apply on top of the shared `.claude/CLAUDE.md`. Use this for preferences like MCP tool mandates, git workflow rules, or personal conventions. Both files load at session start. + +### Personal rules + +Create rules at `~/.claude/rules/` for conventions that apply across all your projects. Project rules in `.claude/rules/` and personal rules in `~/.claude/rules/` both load — they don't override each other. + +### MCP servers + +The shared setup doesn't require any MCP servers. Skills use the `gh` CLI for GitHub operations, which works without MCP. However, MCP servers can enhance your workflow: + +```bash +# GitHub MCP — richer GitHub integration beyond what gh CLI provides +claude mcp add --transport http github https://api.github.com/mcp + +# Semantic code search — understand code structure, not just text patterns +claude mcp add --transport stdio serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context=claude-code --project-from-cwd + +# Documentation search — look up third-party library docs +claude mcp add --transport stdio context7 -- npx -y @upstash/context7-mcp@latest +``` + +After adding an MCP server, grant its tools in your local settings: +```json +{ + "permissions": { + "allow": ["mcp__github__*", "mcp__serena__*", "mcp__context7__*"] + } +} +``` + +### Plugins + +Plugins bundle skills, agents, hooks, and MCP configs. Browse and install from the marketplace: + +```bash +claude plugins list # Browse available plugins +claude plugins install # Install a plugin +claude plugins remove # Remove a plugin +``` + +Useful plugins for Fleet development: `gopls-lsp` (Go LSP), `typescript-lsp` (TS LSP), `feature-dev` (code explorer, architect, and reviewer agents), and `security-guidance` (security warnings on sensitive patterns). + +### Override precedence summary + +| What | Personal location | Behavior | +|------|------------------|----------| +| Skills | `~/.claude/skills//SKILL.md` | Replaces the project skill with the same name | +| Agents | `~/.claude/agents/.md` | Replaces the project agent with the same name | +| Rules | `~/.claude/rules/.md` | Additive — loads alongside project rules | +| Settings | `.claude/settings.local.json` | Merges with project settings; deny rules can't be overridden | +| Hooks | `.claude/settings.local.json` | Additive — runs alongside project hooks | +| CLAUDE.md | Root `CLAUDE.md` (gitignored) | Additive — loads alongside `.claude/CLAUDE.md` | +| Memory | `~/.claude/projects/*/memory/` | Personal only — not shared | + +## Contribute to this configuration + +1. Create a branch. +2. Edit files in `.claude/`. +3. Start a new Claude Code session to test. Use `/context` to verify your changes load correctly. +4. Open a PR for review. + +### Add a skill + +Create `.claude/skills/your-skill/SKILL.md`: +```yaml +--- +name: your-skill +description: When to trigger. Use when asked to "do X" or "Y". +allowed-tools: Read, Grep, Glob, Bash(specific command*) +disable-model-invocation: true # Optional: user-only, no auto-trigger +context: fork # Optional: run in isolated subagent +--- + +Instructions for Claude when this skill is invoked. +Use $ARGUMENTS for user input. +``` + +### Add a rule + +Create `.claude/rules/your-rule.md`: +```yaml +--- +paths: + - "path/**/*.ext" +--- + +# Rule title +- Convention 1 +- Convention 2 +``` + +### Add an agent + +Create `.claude/agents/your-agent.md`: +```yaml +--- +name: your-agent +description: What it does. Include "PROACTIVELY" for auto-invocation. +tools: Read, Grep, Glob, Bash +model: sonnet # or opus for deep reasoning +--- + +System prompt describing the agent's role and review criteria. +``` diff --git a/.claude/agents/fleet-security-auditor.md b/.claude/agents/fleet-security-auditor.md new file mode 100644 index 0000000000..595366cc81 --- /dev/null +++ b/.claude/agents/fleet-security-auditor.md @@ -0,0 +1,60 @@ +--- +name: fleet-security-auditor +description: Fleet-specific security analysis covering MDM, osquery, API auth, and device management threat models. Use when touching auth, MDM, enrollment, or user data. +tools: Read, Grep, Glob, Bash +model: opus +--- + +You are a security engineer specializing in the Fleet codebase. Think like an attacker targeting a device management platform that controls thousands of endpoints. + +## Fleet-Specific Threat Categories + +### API Authorization +- Missing `svc.authz.Authorize(ctx, entity, fleet.ActionX)` calls in service methods +- Privilege escalation between teams (team admin accessing another team's data) +- IDOR (insecure direct object references) on host, policy, or query IDs +- Viewer context: always derive user identity from `viewer.FromContext(ctx)`, never from request data + +### MDM Profile Payloads +- Malicious configuration profiles (Apple .mobileconfig, Windows .xml, Android .json) +- Profile injection that could modify device security settings +- Certificate payloads with untrusted or self-signed certs +- DDM declaration validation against Apple reference + +### osquery Query Injection +- SQL injection through scheduled queries or live query parameters +- Queries accessing sensitive host data beyond intended scope +- Query result exfiltration through webhook or logging channels + +### Enrollment & Secrets +- Enrollment secret exposure in API responses or logs +- Enrollment secret scoping (must be team-specific, not global) +- Orbit agent authentication token handling + +### Certificate & SCEP Handling +- Private key exposure in logs, responses, or error messages +- Certificate chain validation completeness +- SCEP challenge password handling + +### Team Permission Boundaries +- Cross-team data leakage in list/search endpoints +- Team isolation violations in batch operations +- Global vs team-scoped resource access + +### License Enforcement +- Enterprise features accessible without valid license +- License check bypasses in API or service layer + +### PII & Sensitive Data +- Host identifiers, serial numbers, or user emails in log output +- Sensitive MDM payloads in error messages +- Enrollment secrets or API tokens in debug logging + +## Output Format + +For each finding: +- **Severity**: CRITICAL / HIGH / MEDIUM / LOW +- **Location**: File and line +- **Vulnerability**: What the issue is +- **Exploit scenario**: How an attacker could exploit this in a Fleet deployment +- **Fix**: Specific remediation diff --git a/.claude/agents/frontend-reviewer.md b/.claude/agents/frontend-reviewer.md new file mode 100644 index 0000000000..7862b95527 --- /dev/null +++ b/.claude/agents/frontend-reviewer.md @@ -0,0 +1,48 @@ +--- +name: frontend-reviewer +description: Reviews React/TypeScript frontend changes in Fleet for conventions, type safety, component structure, and accessibility. Run PROACTIVELY after modifying frontend files. +tools: Read, Grep, Glob, Bash +model: sonnet +--- + +You are a frontend code reviewer specialized in Fleet's React/TypeScript codebase. Review changes with knowledge of Fleet's specific patterns and conventions. + +## What you check + +### TypeScript strictness +- No `any` types — use `unknown` with type guards or proper interfaces +- Interfaces from `frontend/interfaces/` used correctly (IHost, IUser, etc.) +- Proper type narrowing before accessing nullable fields + +### React Query patterns +- `useQuery` with proper `[queryKey, dependency]` array and `enabled` option +- `useMutation` for write operations +- No manual useState/useEffect for data fetching when React Query is appropriate + +### Component structure +- Follows 4-file pattern: `ComponentName.tsx`, `_styles.scss`, `ComponentName.tests.tsx`, `index.ts` +- New components created with `./frontend/components/generate -n Name -p path` +- Proper named exports (not default exports for new code) + +### SCSS / BEM conventions +- `const baseClass = "component-name"` defined at top +- BEM elements: `${baseClass}__element` +- BEM modifiers: `${baseClass}--modifier` +- Styles in `_styles.scss` files + +### API service usage +- Uses `sendRequest` from `frontend/services/` +- Endpoint constants from `frontend/utilities/endpoints.ts` +- Proper error handling for API calls + +### Accessibility +- ARIA attributes on interactive elements +- Keyboard navigation support +- Semantic HTML elements + +## Output format + +Organize findings by severity: +1. **Blocking** — must fix before merge (type errors, broken patterns, accessibility violations) +2. **Important** — should fix (convention violations, missing types) +3. **Minor** — style nits and suggestions diff --git a/.claude/agents/go-reviewer.md b/.claude/agents/go-reviewer.md index 5b55679ba8..7260c4e643 100644 --- a/.claude/agents/go-reviewer.md +++ b/.claude/agents/go-reviewer.md @@ -1,3 +1,10 @@ +--- +name: go-reviewer +description: Reviews Go code changes in Fleet for bugs, conventions, and security. Run PROACTIVELY after modifying Go files. +tools: Read, Grep, Glob, Bash +model: sonnet +--- + # Go Code Reviewer for Fleet You are a Go code reviewer specialized in the Fleet codebase. Review code changes with deep knowledge of Fleet's patterns and conventions. diff --git a/.claude/commands/project.md b/.claude/commands/project.md deleted file mode 100644 index eb8ca8187c..0000000000 --- a/.claude/commands/project.md +++ /dev/null @@ -1,38 +0,0 @@ -Read the project context file at `~/.fleet/claude-projects/$ARGUMENTS.md`. This contains background, decisions, and conventions for a specific workstream within Fleet. - -Also check for a project-specific memory file named `$ARGUMENTS.md` in your auto memory directory (the persistent memory directory mentioned in your system instructions). If it exists, read it too — it contains things learned while working on this project in previous sessions. - -If the project context file was found, give a brief summary of what you know and ask what we're working on today. - -If the project context file doesn't exist: -1. Tell the user no project named "$ARGUMENTS" was found. -2. List any existing `.md` files in `~/.fleet/claude-projects/` so they can see what's available. -3. Ask if they'd like to initialize a new project with that name. -4. If they don't want to initialize, stop here. -5. If they do, ask them to brain-dump everything they know about the workstream — the goal, what areas of the codebase it touches, key decisions, gotchas, anything they've been repeating at the start of each session. A sentence is fine, a paragraph is better. Also offer: "I can also scan your recent session transcripts for relevant context — would you like me to look back through recent chats?" -6. If they want you to scan prior sessions, look at the JSONL transcript files in the Claude project directory (the same directory as your auto memory, but the `.jsonl` files). Read recent ones (last 5-10), skimming for messages related to the workstream. These are large files, so read selectively — check the first few hundred lines of each to gauge relevance before reading more deeply. -7. Using their description, any prior session context, and codebase exploration, find relevant files, patterns, types, and existing implementations related to the workstream. -8. Create `~/.fleet/claude-projects/$ARGUMENTS.md` populated with what you found, using this structure: - -```markdown -# Project: $ARGUMENTS - -## Background - - -## How It Works - - -## Key Files - - -## Key Decisions - - -## Status - -``` - -9. Show the user what you wrote and ask if they'd like to adjust anything before continuing. - -As you work on a project, update the memory file (in your auto memory directory, named `$ARGUMENTS.md`) with useful discoveries — gotchas, important file paths, patterns — but not session-specific details. diff --git a/.claude/commands/test.md b/.claude/commands/test.md deleted file mode 100644 index 393f64cfdb..0000000000 --- a/.claude/commands/test.md +++ /dev/null @@ -1,10 +0,0 @@ -Run Go tests related to my recent changes. Look at `git diff` and `git diff --cached` to determine which packages were modified. - -For each modified package, run the tests with appropriate env vars: -- If the package is under `server/datastore/mysql`: use `MYSQL_TEST=1` -- If the package is under `server/service`: use `MYSQL_TEST=1 REDIS_TEST=1` -- Otherwise: run without special env vars - -If an argument is provided, use it as a `-run` filter: $ARGUMENTS - -Show a summary of results: which packages passed, which failed, and any failure details. diff --git a/.claude/goimports.sh b/.claude/goimports.sh new file mode 100755 index 0000000000..c5d9699923 --- /dev/null +++ b/.claude/goimports.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# PostToolUse hook: run goimports on Go files after Edit/Write +# Receives tool event JSON on stdin + +INPUT=$(cat) +# Extract file_path with grep to avoid jq parse errors from control chars in tool input +FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') + +if [ -z "$FILE_PATH" ]; then + exit 0 +fi + +case "$FILE_PATH" in + *.go) + if command -v goimports >/dev/null 2>&1; then + goimports -w "$FILE_PATH" 2>/dev/null + elif command -v gofumpt >/dev/null 2>&1; then + gofumpt -w "$FILE_PATH" 2>/dev/null + else + gofmt -w "$FILE_PATH" 2>/dev/null + fi + ;; +esac + +exit 0 diff --git a/.claude/guard-dangerous-commands.sh b/.claude/guard-dangerous-commands.sh new file mode 100755 index 0000000000..dc75361ff7 --- /dev/null +++ b/.claude/guard-dangerous-commands.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# PreToolUse hook: block dangerous bash commands +# Exit 0 = allow, Exit 2 = block + +INPUT=$(cat) +# Extract command with grep to avoid jq parse errors from control chars in tool input +COMMAND=$(printf '%s' "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"command"[[:space:]]*:[[:space:]]*"//;s/"$//') + +if [ -z "$COMMAND" ]; then + exit 0 +fi + +# Block rm -rf with dangerous targets (/, ~, *, bare . but not ./path) +echo "$COMMAND" | grep -qE 'rm\s+-rf\s+/' && { + echo "BLOCKED: rm -rf with absolute path" >&2 + exit 2 +} +echo "$COMMAND" | grep -qE 'rm\s+-rf\s+~' && { + echo "BLOCKED: rm -rf home directory" >&2 + exit 2 +} +echo "$COMMAND" | grep -qE 'rm\s+-rf\s+\*' && { + echo "BLOCKED: rm -rf wildcard" >&2 + exit 2 +} +echo "$COMMAND" | grep -qE 'rm\s+-rf\s+\.$' && { + echo "BLOCKED: rm -rf current directory" >&2 + exit 2 +} + +# Block force push to main/master +echo "$COMMAND" | grep -qiE 'git\s+push\s+.*(--force|-f)\s+.*(main|master)' && { + echo "BLOCKED: force push to main/master" >&2 + exit 2 +} + +# Block hard reset to remote +echo "$COMMAND" | grep -qiE 'git\s+reset\s+--hard\s+origin/' && { + echo "BLOCKED: hard reset to remote" >&2 + exit 2 +} + +# Block pipe-to-shell +echo "$COMMAND" | grep -qiE '(curl|wget)\s+.*\|\s*(ba)?sh' && { + echo "BLOCKED: pipe to shell" >&2 + exit 2 +} + +exit 0 diff --git a/.claude/hooks/guard-dangerous-commands.sh b/.claude/hooks/guard-dangerous-commands.sh new file mode 100755 index 0000000000..ef6af29d6f --- /dev/null +++ b/.claude/hooks/guard-dangerous-commands.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# PreToolUse hook: block dangerous bash commands +# Exit 0 = allow, Exit 2 = block + +INPUT=$(cat) +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +if [ -z "$COMMAND" ]; then + exit 0 +fi + +# Block rm -rf with dangerous targets (/, ~, *, bare . but not ./path) +echo "$COMMAND" | grep -qE 'rm\s+-rf\s+/' && { + echo "BLOCKED: rm -rf with absolute path" >&2 + exit 2 +} +echo "$COMMAND" | grep -qE 'rm\s+-rf\s+~' && { + echo "BLOCKED: rm -rf home directory" >&2 + exit 2 +} +echo "$COMMAND" | grep -qE 'rm\s+-rf\s+\*' && { + echo "BLOCKED: rm -rf wildcard" >&2 + exit 2 +} +echo "$COMMAND" | grep -qE 'rm\s+-rf\s+\.$' && { + echo "BLOCKED: rm -rf current directory" >&2 + exit 2 +} + +# Block force push to main/master +echo "$COMMAND" | grep -qiE 'git\s+push\s+.*(--force|-f)\s+.*(main|master)' && { + echo "BLOCKED: force push to main/master" >&2 + exit 2 +} + +# Block hard reset to remote +echo "$COMMAND" | grep -qiE 'git\s+reset\s+--hard\s+origin/' && { + echo "BLOCKED: hard reset to remote" >&2 + exit 2 +} + +# Block pipe-to-shell +echo "$COMMAND" | grep -qiE '(curl|wget)\s+.*\|\s*(ba)?sh' && { + echo "BLOCKED: pipe to shell" >&2 + exit 2 +} + +exit 0 diff --git a/.claude/hooks/lint-on-save.sh b/.claude/hooks/lint-on-save.sh new file mode 100755 index 0000000000..0a42df162f --- /dev/null +++ b/.claude/hooks/lint-on-save.sh @@ -0,0 +1,84 @@ +#!/bin/sh +# PostToolUse hook: auto-fix lint issues, then report anything remaining +# Uses the project's own make lint-go-incremental (only checks changes since branching from main) +# Runs after formatters (goimports, prettier) so it only sees convention violations + +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + +if [ -z "$FILE_PATH" ]; then + exit 0 +fi + +# Need to be in the project root for make targets +PROJECT_DIR=$(echo "$INPUT" | jq -r '.cwd // empty') +if [ -z "$PROJECT_DIR" ]; then + PROJECT_DIR="$CLAUDE_PROJECT_DIR" +fi +if [ -n "$PROJECT_DIR" ]; then + cd "$PROJECT_DIR" || exit 0 +fi + +TMPFILE=$(mktemp) +trap 'rm -f "$TMPFILE"' EXIT + +case "$FILE_PATH" in + *.go) + # Skip third_party (with or without leading path) + case "$FILE_PATH" in + third_party/*|*/third_party/*) exit 0 ;; + esac + + # First pass: auto-fix what we can (uses golangci-lint directly for --fix) + PKG_DIR=$(dirname "$FILE_PATH") + if command -v golangci-lint >/dev/null 2>&1; then + golangci-lint run --fix "$PKG_DIR/..." > /dev/null 2>&1 + fi + + # Second pass: use project's incremental linter (only changes since branching from main) + if [ -f Makefile ] && grep -q "lint-go-incremental" Makefile; then + make lint-go-incremental > "$TMPFILE" 2>&1 + elif command -v golangci-lint >/dev/null 2>&1; then + # Fallback if make target isn't available + golangci-lint run "$PKG_DIR/..." > "$TMPFILE" 2>&1 + else + exit 0 + fi + + # Filter out noise (level=warning, command echo, summary) and keep only real violations + # Real violations look like: path/to/file.go:LINE:COL: message (lintername) + VIOLATIONS=$(grep -v "^level=" "$TMPFILE" | grep -v "^\\./" | grep -v "^[0-9]* issues" | grep -v "^$" | grep -E '\.go:[0-9]+:[0-9]+:' | head -20) + + if [ -n "$VIOLATIONS" ]; then + echo "$VIOLATIONS" | jq -Rsc --arg fp "$FILE_PATH" \ + '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: ("make lint-go-incremental found issues after editing " + $fp + ":\n" + .)}}' + fi + ;; + + *.ts|*.tsx) + # Determine eslint binary (prefer local, avoid npx auto-install) + if [ -x ./node_modules/.bin/eslint ]; then + ESLINT="./node_modules/.bin/eslint" + elif command -v npx >/dev/null 2>&1 && npx --no-install eslint --version >/dev/null 2>&1; then + ESLINT="npx --no-install eslint" + else + exit 0 + fi + + if [ -n "$ESLINT" ]; then + # First pass: auto-fix + $ESLINT --fix "$FILE_PATH" > /dev/null 2>&1 + + # Second pass: capture remaining issues (include stderr for config/parser errors) + $ESLINT "$FILE_PATH" > "$TMPFILE" 2>&1 + + if grep -q "error\|warning\|Error:" "$TMPFILE"; then + jq -Rsc --arg fp "$FILE_PATH" \ + '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: ("ESLint found issues after editing " + $fp + ":\n" + .)}}' \ + < "$TMPFILE" + fi + fi + ;; +esac + +exit 0 diff --git a/.claude/hooks/prettier-frontend.sh b/.claude/hooks/prettier-frontend.sh new file mode 100755 index 0000000000..56c2e112bd --- /dev/null +++ b/.claude/hooks/prettier-frontend.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# PostToolUse hook: run prettier on frontend files after Edit/Write +# Receives tool event JSON on stdin + +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + +if [ -z "$FILE_PATH" ]; then + exit 0 +fi + +case "$FILE_PATH" in + *.ts|*.tsx|*.scss|*.css|*.js|*.jsx) + # Use local prettier (avoid npx auto-install over network) + if [ -x ./node_modules/.bin/prettier ]; then + ./node_modules/.bin/prettier --write "$FILE_PATH" 2>/dev/null + elif command -v npx >/dev/null 2>&1 && npx --no-install prettier --version >/dev/null 2>&1; then + npx --no-install prettier --write "$FILE_PATH" 2>/dev/null + fi + ;; +esac + +exit 0 diff --git a/.claude/lint-on-save.sh b/.claude/lint-on-save.sh new file mode 100755 index 0000000000..a63edfe812 --- /dev/null +++ b/.claude/lint-on-save.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# PostToolUse hook: auto-fix lint issues, then report anything remaining +# Runs golangci-lint on the affected package (not make lint-go-incremental, which is too +# slow for a PostToolUse hook). Runs after formatters (goimports, prettier) so it only +# sees convention violations. + +INPUT=$(cat) +# Extract file_path with grep to avoid jq parse errors from control chars in tool input +FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') + +if [ -z "$FILE_PATH" ]; then + exit 0 +fi + +# Need to be in the project root for make targets +PROJECT_DIR=$(printf '%s' "$INPUT" | grep -o '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"cwd"[[:space:]]*:[[:space:]]*"//;s/"$//') +if [ -z "$PROJECT_DIR" ]; then + PROJECT_DIR="$CLAUDE_PROJECT_DIR" +fi +if [ -n "$PROJECT_DIR" ]; then + cd "$PROJECT_DIR" || exit 0 +fi + +TMPFILE=$(mktemp) +trap 'rm -f "$TMPFILE"' EXIT + +case "$FILE_PATH" in + *.go) + # Skip third_party (with or without leading path) + case "$FILE_PATH" in + third_party/*|*/third_party/*) exit 0 ;; + esac + + # First pass: auto-fix what we can (uses golangci-lint directly for --fix) + PKG_DIR=$(dirname "$FILE_PATH") + if command -v golangci-lint >/dev/null 2>&1; then + golangci-lint run --fix "$PKG_DIR/..." > /dev/null 2>&1 + fi + + # Second pass: lint the affected package (fast) and report remaining issues + if command -v golangci-lint >/dev/null 2>&1; then + golangci-lint run "$PKG_DIR/..." > "$TMPFILE" 2>&1 + else + exit 0 + fi + + # Filter to real violations: path/to/file.go:LINE:COL: message (lintername) + VIOLATIONS=$(grep -E '\.go:[0-9]+:[0-9]+:' "$TMPFILE" | head -20) + + if [ -n "$VIOLATIONS" ]; then + echo "$VIOLATIONS" | jq -Rsc --arg fp "$FILE_PATH" \ + '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: ("golangci-lint found issues after editing " + $fp + ":\n" + .)}}' + fi + ;; + + *.ts|*.tsx) + # Determine eslint binary (prefer local, avoid npx auto-install) + if [ -x ./node_modules/.bin/eslint ]; then + ESLINT="./node_modules/.bin/eslint" + elif command -v npx >/dev/null 2>&1 && npx --no-install eslint --version >/dev/null 2>&1; then + ESLINT="npx --no-install eslint" + else + exit 0 + fi + + if [ -n "$ESLINT" ]; then + # First pass: auto-fix + $ESLINT --fix "$FILE_PATH" > /dev/null 2>&1 + + # Second pass: capture remaining issues (include stderr for config/parser errors) + $ESLINT "$FILE_PATH" > "$TMPFILE" 2>&1 + + if grep -q "error\|warning\|Error:" "$TMPFILE"; then + jq -Rsc --arg fp "$FILE_PATH" \ + '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: ("ESLint found issues after editing " + $fp + ":\n" + .)}}' \ + < "$TMPFILE" + fi + fi + ;; +esac + +exit 0 diff --git a/.claude/prettier-frontend.sh b/.claude/prettier-frontend.sh new file mode 100755 index 0000000000..bab219967a --- /dev/null +++ b/.claude/prettier-frontend.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# PostToolUse hook: run prettier on frontend files after Edit/Write +# Receives tool event JSON on stdin + +INPUT=$(cat) +# Extract file_path with grep to avoid jq parse errors from control chars in tool input +FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') + +if [ -z "$FILE_PATH" ]; then + exit 0 +fi + +case "$FILE_PATH" in + *.ts|*.tsx|*.scss|*.css|*.js|*.jsx) + # Use local prettier (avoid npx auto-install over network) + if [ -x ./node_modules/.bin/prettier ]; then + ./node_modules/.bin/prettier --write "$FILE_PATH" 2>/dev/null + elif command -v npx >/dev/null 2>&1 && npx --no-install prettier --version >/dev/null 2>&1; then + npx --no-install prettier --write "$FILE_PATH" 2>/dev/null + fi + ;; +esac + +exit 0 diff --git a/.claude/rules/fleet-api.md b/.claude/rules/fleet-api.md new file mode 100644 index 0000000000..2962253f00 --- /dev/null +++ b/.claude/rules/fleet-api.md @@ -0,0 +1,36 @@ +--- +paths: + - "server/service/**/*.go" +--- + +# Fleet API endpoint conventions + +These conventions apply when working on API endpoints in the service layer. Not every file in `server/service/` defines endpoints, but the patterns below should be followed whenever you create or modify one. + +## Endpoint registration +Register endpoints in `server/service/handler.go`: +```go +ue.POST("/api/_version_/fleet/{resource}", endpointFunc, requestType{}) +ue.GET("/api/_version_/fleet/{resource}", endpointFunc, nil) +``` +`_version_` is replaced with the actual API version at runtime. + +## API versioning +- `ue.EndingAtVersion("v1")` — endpoint only available in v1 and earlier +- `ue.StartingAtVersion("2022-04")` — endpoint available from 2022-04 onward +- Current versions: `v1`, `2022-04` +- New endpoints should use `StartingAtVersion("2022-04")` + +## Request body size limits +Use `ue.WithRequestBodySizeLimit(N)` for endpoints accepting large payloads (e.g., bootstrap packages, installers). + +## Error response pattern +Return errors in the response body, not as the second return: +```go +return xResponse{Err: err}, nil // correct +return nil, err // WRONG for Fleet endpoints +``` +Every response struct needs: `func (r xResponse) Error() error { return r.Err }` + +## Reference example +See `server/service/vulnerabilities.go` for a complete example of the request/response/endpoint/service pattern. diff --git a/.claude/rules/fleet-database.md b/.claude/rules/fleet-database.md new file mode 100644 index 0000000000..207d763357 --- /dev/null +++ b/.claude/rules/fleet-database.md @@ -0,0 +1,45 @@ +--- +paths: + - "server/datastore/**/*.go" +--- + +# Fleet Database Conventions + +## Migration Files +- Location: `server/datastore/mysql/migrations/tables/` +- Naming: `YYYYMMDDHHMMSS_CamelCaseName.go` (timestamp + descriptive CamelCase) +- Every migration MUST have a corresponding `_test.go` file +- Structure: + ```go + func init() { + MigrationClient.AddMigration(Up_YYYYMMDDHHMMSS, Down_YYYYMMDDHHMMSS) + } + func Up_YYYYMMDDHHMMSS(tx *sql.Tx) error { ... } + func Down_YYYYMMDDHHMMSS(tx *sql.Tx) error { return nil } // always no-op + ``` +- Test pattern: `applyUpToPrev(t)` → set up data → `applyNext(t, db)` → verify +- Create with: `make migration name=YourChangeName` + +## Query Building +- Use `goqu` (github.com/doug-martin/goqu/v9) for SQL query building +- Pattern: `dialect.From(goqu.I("table_name")).Select(...).Where(...)` +- NEVER use string concatenation for SQL — parameterized queries only +- The `gosec` linter checks for SQL concatenation (G202) + +## Reader vs Writer +- Reads: `ds.reader(ctx)` — may hit a read replica +- Writes: `ds.writer(ctx)` — always hits the primary +- Using the wrong one causes stale reads or replica lag issues + +## Testing +- Integration tests require `MYSQL_TEST=1`: `MYSQL_TEST=1 go test ./server/datastore/mysql/...` +- Use `CreateMySQLDS(t)` helper for test datastore setup +- Table-driven tests with `t.Run` subtests + +## Transactions +- Inside `withTx`/`withRetryTxx` callbacks, use the transaction argument — NEVER call `ds.reader(ctx)` or `ds.writer(ctx)` inside a transaction (custom linter rule catches this) +- Same applies to any function that receives a `sqlx.ExtContext` or `sqlx.ExecContext` as an argument — use that argument, not the datastore's reader/writer + +## Batch Operations +- Use configurable batch size variables for large operations +- Order key allowlists for user-facing sort fields (prevent SQL injection via ORDER BY) diff --git a/.claude/rules/fleet-frontend.md b/.claude/rules/fleet-frontend.md new file mode 100644 index 0000000000..09aede0015 --- /dev/null +++ b/.claude/rules/fleet-frontend.md @@ -0,0 +1,90 @@ +--- +paths: + - "frontend/**/*.ts" + - "frontend/**/*.tsx" +--- + +# Fleet Frontend Conventions + +## Component Structure +Every component should have this 4-file structure: +- `ComponentName.tsx` — Main component +- `_styles.scss` — Component-specific SCSS styles +- `ComponentName.tests.tsx` — Tests +- `index.ts` — Named export + +Use the component generator for new components: +``` +./frontend/components/generate -n PascalCaseName -p optional/path/to/parent +``` + +## React Query +- Use `useQuery` for data fetching with `[queryKey, dependency]` and `enabled` option +- Prefer React Query over manual useState/useEffect for API data +- Use `useMutation` for write operations — invalidate related queries on success +- Query key pattern: `["resource", id, teamId]` — include all dependencies + +## API Services +- API clients live in `frontend/services/entities/` +- Use `sendRequest(method, path, body?, queryParams?)` from `frontend/services/` +- Endpoint constants in `frontend/utilities/endpoints.ts` +- Build query strings with `buildQueryStringFromParams()` from `frontend/utilities/url/` +- Build full paths with `getPathWithQueryParams(path, params)` — auto-filters undefined/null values + +## Permission Checking +Use helpers from `frontend/utilities/permissions/permissions.ts`: +- Global roles: `permissions.isGlobalAdmin(user)`, `isGlobalMaintainer(user)`, `isOnGlobalTeam(user)` +- Team roles: `permissions.isTeamAdmin(user, teamId)`, `isTeamMaintainer(user, teamId)`, `isTeamObserver(user, teamId)` +- Multi-team: `permissions.isAnyTeamAdmin(user)`, `isOnlyObserver(user)` +- License: `permissions.isPremiumTier(config)`, `isFreeTier(config)` +- MDM: `permissions.isMacMdmEnabledAndConfigured(config)`, `isWindowsMdmEnabledAndConfigured(config)` + +## Team Context +Use the `useTeamIdParam` hook for team-scoped pages: +- `currentTeamId`: -1 (All teams), 0 (No team), or positive team ID +- `teamIdForApi`: undefined (All teams), 0 (No team), or positive ID — **always use this for API calls** +- `handleTeamChange(newTeamId)` to switch teams +- `isTeamAdmin`, `isTeamMaintainer`, `isObserverPlus` for role checks + +## Notifications +- Use `renderFlash(alertType, message)` from `NotificationContext` +- Types: `"success"`, `"error"`, `"warning-filled"` +- Use `renderMultiFlash()` for batch operations + +## XSS Prevention +- ALWAYS sanitize user-generated HTML with `DOMPurify.sanitize(html, options)` before `dangerouslySetInnerHTML` +- Configure allowed tags/attributes explicitly: `{ ADD_ATTR: ["target"] }` + +## String Utilities +Use helpers from `frontend/utilities/strings/stringUtils.ts`: +- `capitalize(str)`, `capitalizeRole(role)` — handle special casing (Observer+) +- `pluralize(count, singular, pluralSuffix, singularSuffix)` — "1 host" vs "2 hosts" +- `stripQuotes(str)`, `strToBool(str)` — input parsing +- `enforceFleetSentenceCasing(str)` — respects Fleet stylization rules + +## Styling (SCSS + BEM) +- Define `const baseClass = "component-name"` at the top of the component +- Elements: `` className={`${baseClass}__element-name`} `` +- Modifiers: `` className={`${baseClass}--modifier`} `` +- Use `classnames()` for conditional classes +- Style files use underscore prefix: `_styles.scss` + +## Interfaces & Types +- Interface files live in `frontend/interfaces/` with `I` prefix: `IHost`, `IUser`, `IPack` +- Legacy pattern: some files export both PropTypes (default export) and TypeScript interfaces (named export) +- New code should use TypeScript interfaces only + +## Hooks & Context +- Custom hooks in `frontend/hooks/` — e.g., `useTeamIdParam`, `useCheckboxListStateManagement` +- Context providers in `frontend/context/` — `AppContext` for global state, `NotificationContext` for flash messages + +## Terminology +- "Teams" are now called "fleets" in the product. Code still uses `team_id`, `useTeamIdParam`, `permissions.isTeamAdmin`, etc. — don't rename existing APIs, but use "fleet" in new user-facing strings and comments. +- "Queries" are now called "reports." The word "query" now refers solely to a SQL query. Code still uses `useQuery`, `queryKey`, etc. for React Query — that's unrelated to the product terminology change. + +## Linting & Formatting +- ESLint: extends airbnb + typescript-eslint + prettier +- Prettier: default config (`.prettierrc.json`) +- `console.log` is allowed (`no-console` is off) — useful for debugging, but clean up before merging +- `react-hooks/exhaustive-deps` is enforced as a warning — include all dependencies in hook dependency arrays +- Run `make lint-js` or `yarn lint` and `npx prettier --check frontend/` before submitting diff --git a/.claude/rules/fleet-go-backend.md b/.claude/rules/fleet-go-backend.md new file mode 100644 index 0000000000..a285bbfc1b --- /dev/null +++ b/.claude/rules/fleet-go-backend.md @@ -0,0 +1,105 @@ +--- +paths: + - "server/**/*.go" + - "cmd/**/*.go" + - "orbit/**/*.go" + - "ee/**/*.go" + - "pkg/**/*.go" + - "tools/**/*.go" + - "client/**/*.go" + - "test/**/*.go" +--- + +# Fleet Go Backend Conventions + +## Error Handling +- Wrap errors with `ctxerr.Wrap(ctx, err, "description")` — never `pkg/errors` or `fmt.Errorf` with `%w` +- For error messages without wrapping, use `errors.New("msg")` not `fmt.Errorf("msg")` (the linter catches this) +- Banned imports: `github.com/pkg/errors`, `github.com/valyala/fastjson`, `github.com/valyala/fasttemplate` +- Use the right error type for the right situation: + - `fleet.NewInvalidArgumentError(field, reason)` — input validation (422). Accumulate with `.Append(field, reason)`, check `.HasErrors()` + - `&fleet.BadRequestError{Message: "..."}` — malformed request (400) + - `fleet.NewAuthFailedError()` / `fleet.NewAuthRequiredError()` — auth failures (401) + - `fleet.NewPermissionError(msg)` — authorized but insufficient role (403) + - Implement `IsNotFound() bool` interface — resource not found. Check with `fleet.IsNotFound(err)` + - `&fleet.ConflictError{Message: "..."}` — duplicate/conflict (409) +- Check error types with: `fleet.IsNotFound(err)`, `fleet.IsAlreadyExists(err)` + +## Input Validation +- Validate in service methods, not in endpoint functions +- Accumulate all errors before returning: + ```go + invalid := fleet.NewInvalidArgumentError("name", "cannot be empty") + if badCondition { + invalid.Append("email", "must be valid") + } + if invalid.HasErrors() { + return invalid + } + ``` + +## Service Methods +- Signature: `func (svc *Service) MethodName(ctx context.Context, ...) (..., error)` +- Start with authorization: `svc.authz.Authorize(ctx, &fleet.Entity{}, fleet.ActionX)` +- For entity-specific auth, double-authorize: generic check first, load entity, then team-scoped check: + ```go + if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionRead); err != nil { return nil, err } + host, err := svc.ds.Host(ctx, hostID) + if err != nil { return nil, ctxerr.Wrap(ctx, err, "get host") } + if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil { return nil, err } + ``` +- Return errors via ctxerr wrapping + +## Viewer Context +- Get current user: `vc, ok := viewer.FromContext(ctx)` — NEVER trust user identity from request body +- Helpers: `vc.UserID()`, `vc.Email()`, `vc.IsLoggedIn()`, `vc.CanPerformActions()` +- System operations: `viewer.NewSystemContext(ctx)` for admin-level automated actions + +## Pagination +- Use `fleet.ListOptions` for all list endpoints (Page, PerPage, OrderKey, OrderDirection, MatchQuery, After) +- Return `*fleet.PaginationMetadata` when `IncludeMetadata` is true +- Cursor pagination: check `ListOptions.UsesCursorPagination()` + +## Request/Response Pattern +- Request structs: lowercase type, json/url tags: `type listEntitiesRequest struct` +- Response structs: include `Err error` field and `func (r xResponse) Error() error { return r.Err }` +- Endpoint functions: `func xEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error)` +- Errors go in the response body: `return xResponse{Err: err}, nil` + +## Logging +- Use slog with context: `logger.InfoContext(ctx, "message", "key", value)` +- NEVER use bare `slog.Debug`, `slog.Info`, `slog.Warn`, `slog.Error` — the `forbidigo` linter rejects these +- NEVER use `print()` or `println()` — use structured logging + +## Imports & Utilities +- Internal packages: `github.com/fleetdm/fleet/v4/server/` prefix +- **HTTP clients**: Use `fleethttp.NewClient()` — never `http.Client{}` or `new(http.Client)` directly (custom linter rule) +- **Pointers (Go 1.26+)**: Use `new(expression)` for pointer values: `new("value")`, `new(true)`, `new(yearsSince(born))`. Do NOT use the `server/ptr` package (`ptr.String()`, `ptr.Uint()`, etc.) in new code — it's legacy. You'll see it throughout the existing codebase but should not follow that pattern. +- **Random numbers**: use `math/rand/v2` instead of `math/rand` +- Sets: use `map[T]struct{}`, convert to slice with `slices.Collect(maps.Keys(m))` +- Flexible JSON: use `json.RawMessage` for configs stored as JSON blobs + +## Context Utilities +- `ctxdb.RequirePrimary(ctx, true)` — force reads on primary DB (use before read-then-write) +- `ctxdb.BypassCachedMysql(ctx, true)` — disable MySQL cache layer +- `ctxerr.Wrap(ctx, err, "msg")` — ALWAYS use for error wrapping + +## Testing +- Use `require` and `assert` from `github.com/stretchr/testify` +- Mock invocation tracking: check `ds.{FuncName}FuncInvoked` bool (auto-set by generated mocks) +- Run `go test ./server/service/` after adding new datastore interface methods — uninitialized mocks crash other tests +- Integration tests need `MYSQL_TEST=1 REDIS_TEST=1` +- Use `t.Context()` instead of `context.Background()` + +## Bounded contexts + +Some domains use a self-contained bounded context pattern instead of the traditional `fleet/` → `service/` → `datastore/` layers: +- `server/activity/` — internal types, mysql, service, API, and bootstrap in one directory +- `server/mdm/` — similar self-contained structure for MDM + +When working in these directories, follow the local patterns (internal packages, local types) rather than the top-level Fleet architecture. + +## Linting +- Follow `.golangci.yml` — enabled linters: depguard, forbidigo, gosec, gocritic, revive, errcheck, staticcheck +- After editing: `make lint-go-incremental` (only checks changes since branching from main) +- Before committing: `make lint-go` (full lint) diff --git a/.claude/rules/fleet-orbit.md b/.claude/rules/fleet-orbit.md new file mode 100644 index 0000000000..096d51d38a --- /dev/null +++ b/.claude/rules/fleet-orbit.md @@ -0,0 +1,40 @@ +--- +paths: + - "orbit/**/*.go" +--- + +# Fleet Orbit conventions + +Orbit is Fleet's lightweight agent that manages osquery, handles updates, and provides device-level functionality. It runs on end-user devices, so reliability and security are critical. + +## Architecture +- **Entry point**: `orbit/cmd/orbit/` — main binary +- **Packages**: `orbit/pkg/` — modular packages for each concern +- **Update system**: `orbit/pkg/update/` — TUF-based auto-update for osquery, orbit, and desktop +- **Packaging**: `orbit/pkg/packaging/` — builds installers for macOS (.pkg), Windows (.msi), and Linux (.deb/.rpm) +- **Platform-specific code**: use build tags (`_darwin.go`, `_windows.go`, `_linux.go`) and `_stub.go` for unsupported platforms + +## Key patterns +- **Keystore**: `orbit/pkg/keystore/` — platform-specific secure key storage (macOS Keychain, Windows DPAPI, Linux file-based). Always use the keystore abstraction, never raw file I/O for secrets. +- **osquery management**: `orbit/pkg/osquery/` — launching, monitoring, and communicating with osquery. Orbit owns the osquery lifecycle. +- **Token management**: `orbit/pkg/token/` — orbit enrollment token read/write with file locking +- **Platform executables**: `orbit/pkg/execuser/` — run commands as the logged-in user (not root). Critical for UI prompts and desktop app. + +## Security considerations +- Orbit runs as root/SYSTEM — every input must be validated +- Never log enrollment tokens, orbit keys, or device identifiers at info level +- File operations on device should use restrictive permissions (0600/0700) +- TUF update verification must never be bypassed +- Use `orbit/pkg/insecure/` only for intentionally insecure test configurations + +## Testing +- Unit tests don't need special env vars (no MySQL/Redis) +- Platform-specific tests may need build tags: `go test -tags darwin ./orbit/pkg/...` +- Use `_stub.go` files for cross-platform test compatibility +- Packaging tests may require signing certificates or specific tools (notarytool, WiX) + +## Build and packaging +- macOS: `.pkg` built with `pkgbuild`, optional notarization via `notarytool` or `rcodesign` +- Windows: `.msi` built with WiX toolset, templates in `orbit/pkg/packaging/windows_templates.go` +- Linux: `.deb` and `.rpm` via `nfpm` +- Cross-compilation: orbit supports `GOOS`/`GOARCH` targeting diff --git a/.claude/settings.json b/.claude/settings.json index 325ed4638f..393f4d9b67 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -11,13 +11,76 @@ "allow": [ "Read(~/.fleet/claude-projects/**)", "Write(~/.fleet/claude-projects/**)", - "Edit(~/.fleet/claude-projects/**)" + "Edit(~/.fleet/claude-projects/**)", + "Bash(go test*)", + "Bash(go vet*)", + "Bash(go build*)", + "Bash(go fmt*)", + "Bash(gofmt*)", + "Bash(golangci-lint *)", + "Bash(MYSQL_TEST=1 go test*)", + "Bash(MYSQL_TEST=1 REDIS_TEST=1 go test*)", + "Bash(FLEET_INTEGRATION_TESTS_DISABLE_LOG=1 *)", + "Bash(yarn test*)", + "Bash(yarn lint*)", + "Bash(npx prettier*)", + "Bash(npx eslint*)", + "Bash(npx tsc*)", + "Bash(npx jest*)", + "Bash(make test*)", + "Bash(make lint*)", + "Bash(make build*)", + "Bash(make mock*)", + "Bash(make generate*)", + "Bash(make serve*)", + "Bash(make up*)", + "Bash(make db-*)", + "Bash(make migration*)", + "Bash(make deps*)", + "Bash(make e2e-*)", + "Bash(make run-go-tests*)", + "Bash(make fleet-dev*)", + "Bash(make fleetctl-dev*)", + "Bash(make clean*)", + "Bash(make doc*)", + "Bash(make dump-test-schema*)", + "Bash(make analyze-go*)", + "Bash(make update-go*)", + "Bash(make check-go*)", + "Bash(git status*)", + "Bash(git diff*)", + "Bash(git log*)", + "Bash(git show*)", + "Bash(git branch*)", + "Bash(gh pr *)", + "Bash(gh issue *)", + "Bash(gh run *)", + "Bash(gh api *)" + ], + "deny": [ + "Bash(git push --force*)", + "Bash(git push -f*)", + "Bash(rm -rf /*)", + "Bash(rm -rf ~*)" ] }, "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-dangerous-commands.sh", + "timeout": 5 + } + ] + } + ], "PostToolUse": [ { "matcher": "Edit|Write", + "if": "Edit(**/*.go) || Write(**/*.go)", "hooks": [ { "type": "command", @@ -25,6 +88,28 @@ "timeout": 10 } ] + }, + { + "matcher": "Edit|Write", + "if": "Edit(frontend/**) || Write(frontend/**)", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/prettier-frontend.sh", + "timeout": 10 + } + ] + }, + { + "matcher": "Edit|Write", + "if": "Edit(**/*.go) || Edit(**/*.ts) || Edit(**/*.tsx) || Write(**/*.go) || Write(**/*.ts) || Write(**/*.tsx)", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-on-save.sh", + "timeout": 60 + } + ] } ] } diff --git a/.claude/skills/bump-migration/SKILL.md b/.claude/skills/bump-migration/SKILL.md new file mode 100644 index 0000000000..ec3ade9842 --- /dev/null +++ b/.claude/skills/bump-migration/SKILL.md @@ -0,0 +1,58 @@ +--- +name: bump-migration +description: Bump a database migration's timestamp to the current time. Required when a PR's migration is older than one already merged to main. Use when asked to "bump migration", "update migration timestamp", or when a migration ordering conflict is detected. +allowed-tools: Bash(go run *), Bash(make dump-test-schema*), Bash(git diff*), Bash(ls *), Read, Grep, Glob +model: sonnet +effort: medium +--- + +# Bump a database migration timestamp + +Bump the migration: $ARGUMENTS + +## When to use + +This is required when a PR has a database migration with a timestamp older than a migration already merged to main. This happens when a PR has been pending merge for a while and another PR got merged with a more recent migration. + +## Process + +### 1. Identify the migration to bump + +If the user provided a filename, use that. Otherwise, find migrations on this branch that are older than the latest on main: + +```bash +# List migrations on this branch that aren't on main +git diff origin/main --name-only -- server/datastore/mysql/migrations/tables/ +``` + +### 2. Run the bump tool + +The tool lives at `tools/bump-migration/main.go`. Run it from the repo root: + +```bash +go run tools/bump-migration/main.go --source-migration YYYYMMDDHHMMSS_MigrationName.go +``` + +This will: +- Rename the migration file with a new current timestamp +- Rename the test file (if it exists) +- Update all function names inside both files (`Up_OLDTS` → `Up_NEWTS`, `Down_OLDTS` → `Down_NEWTS`, `TestUp_OLDTS` → `TestUp_NEWTS`) + +### 3. Optionally regenerate the schema + +If the migration affects the schema, add `--regen-schema` to also run `make dump-test-schema`: + +```bash +go run tools/bump-migration/main.go --source-migration YYYYMMDDHHMMSS_MigrationName.go --regen-schema +``` + +### 4. Verify + +- Check that the old files are gone and new files exist with the updated timestamp +- Verify the function names inside the files match the new timestamp +- Run `go build ./server/datastore/mysql/migrations/...` to check compilation + +## Rules +- Always run from the repo root +- Provide the migration filename, not the test filename +- The tool handles both the migration and its test file automatically diff --git a/.claude/commands/find-related-tests.md b/.claude/skills/find-related-tests/SKILL.md similarity index 69% rename from .claude/commands/find-related-tests.md rename to .claude/skills/find-related-tests/SKILL.md index 957703779c..e8eb9ad07d 100644 --- a/.claude/commands/find-related-tests.md +++ b/.claude/skills/find-related-tests/SKILL.md @@ -1,3 +1,10 @@ +--- +name: find-related-tests +description: Find test files and functions related to recent git changes. Suggests exact go test commands with correct env vars. +allowed-tools: Bash(git *), Read, Grep, Glob +effort: low +--- + Look at my recent git changes (`git diff` and `git diff --cached`) and find all related test files. For each modified file, find: diff --git a/.claude/commands/fix-ci.md b/.claude/skills/fix-ci/SKILL.md similarity index 92% rename from .claude/commands/fix-ci.md rename to .claude/skills/fix-ci/SKILL.md index b8e731bdeb..332b9d22b7 100644 --- a/.claude/commands/fix-ci.md +++ b/.claude/skills/fix-ci/SKILL.md @@ -1,3 +1,11 @@ +--- +name: fix-ci +description: Diagnose and fix failing CI tests from a GitHub Actions run. Use when asked to "fix CI", "CI failure", or "failing tests in CI". +allowed-tools: Bash(gh *), Bash(go test *), Bash(go build *), Bash(MYSQL_TEST*), Bash(MYSQL_TEST=1 REDIS_TEST=1 *), Bash(FLEET_INTEGRATION_TESTS_DISABLE_LOG=1 *), Read, Grep, Glob, Edit +model: opus +effort: high +--- + Fix failing tests from a CI run. The argument is a GitHub Actions run URL or run ID: $ARGUMENTS ## Step 1: Identify failing jobs diff --git a/.claude/commands/fleet-gitops.md b/.claude/skills/fleet-gitops/SKILL.md similarity index 90% rename from .claude/commands/fleet-gitops.md rename to .claude/skills/fleet-gitops/SKILL.md index 3a3b6829e8..2b06fb287b 100644 --- a/.claude/commands/fleet-gitops.md +++ b/.claude/skills/fleet-gitops/SKILL.md @@ -1,3 +1,10 @@ +--- +name: fleet-gitops +description: Help with Fleet GitOps configuration files including queries, profiles, software, and DDM declarations with validation against upstream references. +allowed-tools: Read, Grep, Glob, Edit, Write, WebFetch, WebSearch +effort: high +--- + You are helping with Fleet GitOps configuration files: $ARGUMENTS Focus on the `it-and-security` folder. Apply the following constraints for all work in this session. diff --git a/.claude/skills/lint/SKILL.md b/.claude/skills/lint/SKILL.md new file mode 100644 index 0000000000..cb164054fd --- /dev/null +++ b/.claude/skills/lint/SKILL.md @@ -0,0 +1,69 @@ +--- +name: lint +description: Run linters on recently changed files with the correct tools for each language. Use when asked to "lint", "check style", or "run linters". +allowed-tools: Bash(make lint*), Bash(golangci-lint *), Bash(go vet*), Bash(yarn lint*), Bash(yarn --cwd *), Bash(npx eslint*), Bash(npx prettier*), Bash(git diff*), Bash(git status*), Read, Grep, Glob +effort: low +--- + +# Lint recent changes + +Run the appropriate linters on files changed in the current branch. Use the project's own make targets when available. + +## Process + +### 1. Detect changed files + +Find recently changed files (last commit, staged, and unstaged): + +```bash +git diff --name-only HEAD~1 # Last commit +git diff --name-only --cached # Staged but not committed +git diff --name-only # Unstaged changes +``` + +Combine all three and deduplicate to get the full set. + +### 2. Run linters by language + +**Go files** (`*.go`): +Use the project's incremental linter — it only checks changes since branching from main: +```bash +make lint-go-incremental +``` +This uses `.golangci-incremental.yml` with `--new-from-merge-base=origin/main`. It's faster and more relevant than linting entire packages. + +For a full lint (e.g., before committing), use: +```bash +make lint-go +``` + +**TypeScript/JavaScript files** (`*.ts`, `*.tsx`, `*.js`, `*.jsx`): +```bash +npx eslint frontend/path/to/changed/files +npx prettier --check frontend/path/to/changed/files +``` + +Or use the make target: +```bash +make lint-js +``` + +**SCSS files** (`*.scss`): +```bash +npx prettier --check frontend/path/to/changed/files.scss +``` + +### 3. Report results + +For each linter run, show: +- Which packages/files were linted +- Any errors or warnings found +- Suggested fixes (if the linter provides them) + +If everything passes, confirm which linters ran and on which files. + +If an argument is provided, use it to filter: $ARGUMENTS +- `go` — only Go linters (uses `make lint-go-incremental`) +- `full` — full Go lint (uses `make lint-go`) +- `js` or `frontend` — only frontend linters (uses `make lint-js`) +- A file path — lint that specific file/package diff --git a/.claude/skills/new-endpoint/SKILL.md b/.claude/skills/new-endpoint/SKILL.md new file mode 100644 index 0000000000..1e82ddff3e --- /dev/null +++ b/.claude/skills/new-endpoint/SKILL.md @@ -0,0 +1,82 @@ +--- +name: new-endpoint +description: Scaffold a new Fleet API endpoint with request/response structs, endpoint function, service method, datastore interface, handler registration, and test stubs. +allowed-tools: Read, Write, Edit, Grep, Glob +model: sonnet +effort: high +disable-model-invocation: true +--- + +# Scaffold a New Fleet API Endpoint + +Create a new API endpoint for: $ARGUMENTS + +## Process + +### 1. Gather Requirements +- Resource name and HTTP method (GET/POST/PATCH/DELETE) +- URL path (e.g., `/api/_version_/fleet/resource`) +- Request body fields (if any) +- Response body fields +- Which API version (use `StartingAtVersion("2022-04")` for new endpoints) +- Does it need a datastore method? + +### 2. Read Reference Patterns +Read `server/service/vulnerabilities.go` for the canonical request/response/endpoint pattern: +- Request struct with json tags +- Response struct with `Err error` field and `Error()` method +- Endpoint function with `(ctx, request, svc)` signature + +Read `server/service/handler.go` to find where to register the new endpoint. + +### 3. Create Request/Response Structs +```go +type myResourceRequest struct { + ID uint `url:"id"` + Name string `json:"name"` +} + +type myResourceResponse struct { + Resource *fleet.Resource `json:"resource,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r myResourceResponse) Error() error { return r.Err } +``` + +### 4. Create Endpoint Function +```go +func myResourceEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { + req := request.(*myResourceRequest) + result, err := svc.MyResource(ctx, req.ID) + if err != nil { + return myResourceResponse{Err: err}, nil + } + return myResourceResponse{Resource: result}, nil +} +``` + +### 5. Add Service Interface Method +In `server/fleet/service.go`, add the method to the `Service` interface. + +### 6. Implement Service Method +In the appropriate `server/service/*.go` file: +- Start with `svc.authz.Authorize(ctx, &fleet.Entity{}, fleet.ActionRead)` +- Implement business logic +- Wrap errors with `ctxerr.Wrap` + +### 7. Add Datastore Interface Method (if needed) +In `server/fleet/datastore.go`, add the method to the `Datastore` interface. + +### 8. Register in handler.go +```go +ue.StartingAtVersion("2022-04").GET("/api/_version_/fleet/resource", myResourceEndpoint, myResourceRequest{}) +``` + +### 9. Create Test Stubs +- Unit test with mock datastore in `server/service/*_test.go` +- Integration test stub if it touches the database + +### 10. Verify +- Run `go build ./...` to check compilation +- Run `go test ./server/service/` to check mocks are satisfied diff --git a/.claude/skills/new-migration/SKILL.md b/.claude/skills/new-migration/SKILL.md new file mode 100644 index 0000000000..154b05456a --- /dev/null +++ b/.claude/skills/new-migration/SKILL.md @@ -0,0 +1,78 @@ +--- +name: new-migration +description: Create a new Fleet database migration with timestamp naming, Up function, init registration, and test file. +allowed-tools: Bash(date *), Bash(make migration *), Bash(go build *), Bash(go test *), Bash(MYSQL_TEST*), Read, Write, Grep, Glob +model: sonnet +effort: medium +--- + +# Create a New Database Migration + +Create a migration for: $ARGUMENTS + +## Process + +### 1. Generate Timestamp and Name +Use `make migration name=CamelCaseName` if available, or generate manually: +```bash +date +%Y%m%d%H%M%S +``` +The migration name should be descriptive CamelCase (e.g., `AddRecoveryLockAutoRotateAt`, `CreateTableSoftwareInstallers`). + +### 2. Create Migration File +Location: `server/datastore/mysql/migrations/tables/{TIMESTAMP}_{Name}.go` + +```go +package tables + +import "database/sql" + +func init() { + MigrationClient.AddMigration(Up_{TIMESTAMP}, Down_{TIMESTAMP}) +} + +func Up_{TIMESTAMP}(tx *sql.Tx) error { + _, err := tx.Exec(` + -- SQL statement here + `) + return err +} + +func Down_{TIMESTAMP}(tx *sql.Tx) error { + return nil +} +``` + +### 3. Create Test File +Location: `server/datastore/mysql/migrations/tables/{TIMESTAMP}_{Name}_test.go` + +```go +package tables + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUp_{TIMESTAMP}(t *testing.T) { + db := applyUpToPrev(t) + + // Set up test data before migration if needed + + applyNext(t, db) + + // Verify migration applied correctly + // e.g., check table exists, columns added, data migrated +} +``` + +### 4. Verify +- Run `go build ./server/datastore/mysql/migrations/...` to check compilation +- Run `MYSQL_TEST=1 go test -run TestUp_{TIMESTAMP} ./server/datastore/mysql/migrations/tables/` to test the migration + +## Rules +- Every migration MUST have a test file +- Down migrations are always no-ops (`return nil`) — Fleet doesn't use rollback migrations +- Never modify existing migration files — create new ones +- Data migrations go in the `data/` subdirectory diff --git a/.claude/skills/project/SKILL.md b/.claude/skills/project/SKILL.md new file mode 100644 index 0000000000..b34c5bbada --- /dev/null +++ b/.claude/skills/project/SKILL.md @@ -0,0 +1,58 @@ +--- +name: project +description: Load or initialize a Fleet workstream project context. Use when asked to "load project" or "switch project". +context: fork +allowed-tools: Read, Write, Glob, Grep, Bash(ls *), Bash(pwd *) +effort: medium +--- + +# Load a workstream project context + +## Detect the project directory + +Find the Claude Code auto-memory directory for this project. It's based on the working directory path: + +1. Run `pwd` to get the current directory. +2. Construct the memory path: `~/.claude/projects/` + the cwd with `/` replaced by `-` and leading `-` (e.g., `/Users/alice/Source/github.com/fleetdm/fleet` → `~/.claude/projects/-Users-alice-Source-github-com-fleetdm-fleet/memory/`). +3. Verify the directory exists. If not, tell the user and stop. + +Use this as the base for all reads and writes below. + +## Load the project + +Look for a workstream context file named `$ARGUMENTS.md` in the memory directory. This contains background, decisions, and conventions for a specific workstream within Fleet. + +If the project context file was found, give a brief summary of what you know and ask what we're working on today. + +If the project context file doesn't exist: +1. Tell the user no project named "$ARGUMENTS" was found. +2. List any existing `.md` files in the memory directory so they can see what's available. +3. Ask if they'd like to initialize a new project with that name. +4. If they don't want to initialize, stop here. +5. If they do, ask them to brain-dump everything they know about the workstream — the goal, what areas of the codebase it touches, key decisions, gotchas, anything they've been repeating at the start of each session. A sentence is fine, a paragraph is better. Also offer: "I can also scan your recent session transcripts for relevant context — would you like me to look back through recent chats?" +6. If they want you to scan prior sessions, look at the JSONL transcript files in the Claude project directory (the parent of the memory directory). Read recent ones (last 5-10), skimming for messages related to the workstream. These are large files, so read selectively — check the first few hundred lines of each to gauge relevance before reading more deeply. +7. Using their description, any prior session context, and codebase exploration, find relevant files, patterns, types, and existing implementations related to the workstream. +8. Create the project file in the memory directory using this structure: + +```markdown +# Project: $ARGUMENTS + +## Background + + +## How it works + + +## Key files + + +## Key decisions + + +## Status + +``` + +9. Show the user what you wrote and ask if they'd like to adjust anything before continuing. + +As you work on a project, update the project file with useful discoveries — gotchas, important file paths, patterns — but not session-specific details. diff --git a/.claude/commands/review-pr.md b/.claude/skills/review-pr/SKILL.md similarity index 75% rename from .claude/commands/review-pr.md rename to .claude/skills/review-pr/SKILL.md index ebc1564bcc..235103f9ab 100644 --- a/.claude/commands/review-pr.md +++ b/.claude/skills/review-pr/SKILL.md @@ -1,3 +1,12 @@ +--- +name: review-pr +description: Review a Fleet pull request for correctness, Go idioms, SQL safety, test coverage, and conventions. Use when asked to "review PR" or "review pull request". +context: fork +allowed-tools: Bash(gh *), Read, Grep, Glob +model: opus +effort: high +--- + Review the pull request: $ARGUMENTS Use `gh pr view` and `gh pr diff` to get the full context. diff --git a/.claude/skills/spec-story/SKILL.md b/.claude/skills/spec-story/SKILL.md new file mode 100644 index 0000000000..5c0f7aff04 --- /dev/null +++ b/.claude/skills/spec-story/SKILL.md @@ -0,0 +1,99 @@ +--- +name: spec-story +description: Break down a Fleet GitHub story issue into implementable sub-issues with technical specs. Use when asked to "spec", "break down", or "analyze" a story or issue. +allowed-tools: Bash(gh *), Read, Grep, Glob, Write, Edit, WebFetch(domain:github.com), WebFetch(domain:fleetdm.com), WebSearch +model: opus +effort: high +argument-hint: "" +--- + +# Spec a Fleet Story + +Break down the GitHub story into implementable sub-issues: $ARGUMENTS + +## Process + +### 1. Understand the Story +- Fetch the issue with `gh issue view --json title,body,labels,milestone,assignees` +- Read the full description, acceptance criteria, and any linked issues +- Identify the user-facing goal and success criteria +- If the issue references Figma designs, API docs, or external specs, fetch them + +### 2. Map the Codebase Impact +Search the codebase to understand what exists and what needs to change: +- Find existing implementations of related features (Grep for key terms) +- Identify the tables, service methods, API endpoints, and frontend pages involved +- Check migration files and `server/fleet/datastore.go` for relevant schema +- Trace the request flow: API endpoint → service method → datastore → frontend + +### 3. Identify Sub-Issues +Decompose into atomic, implementable units. Each sub-issue should be: +- Completable independently (or with clearly stated dependencies) +- Testable with specific acceptance criteria +- Scoped to one layer when possible (backend, frontend, or migration) + +Common decomposition patterns for Fleet: +- **Database migration** — new tables or columns needed +- **Datastore methods** — new or modified query functions +- **Service layer** — business logic, authorization, validation +- **API endpoint** — new or modified HTTP endpoints +- **Frontend page/component** — UI changes +- **fleetctl/GitOps** — CLI and GitOps YAML support +- **Tests** — integration test coverage for the feature +- **Documentation** — REST API docs, user-facing docs + +### 4. Write Each Sub-Issue Spec + +For each sub-issue, write: + +```markdown +## Sub-issue N: [Title] + +**Depends on:** [sub-issue numbers, or "none"] +**Layer:** [migration | datastore | service | API | frontend | CLI | docs | tests] +**Estimated scope:** [small: <2h | medium: 2-8h | large: >8h] + +### What +[1-3 sentences describing the change] + +### Why +[How this contributes to the parent story's goal] + +### Technical Approach +- [Specific files to create or modify] +- [Key functions, types, or patterns to follow] +- [Reference existing similar implementations] + +### Acceptance Criteria +- [ ] [Testable criterion 1] +- [ ] [Testable criterion 2] +- [ ] [Tests pass: specific test commands] + +### Open Questions +- [Any ambiguity that needs product/design input] +``` + +### 5. Produce the Dependency Graph +Show which sub-issues depend on which: +``` +Migration → Datastore → Service → API → Frontend + → CLI/GitOps + → Docs +``` +Note which sub-issues can be parallelized. + +### 6. Write the Output +Create a spec document with: +1. **Summary** — one paragraph overview +2. **Sub-issues** — each with the template above +3. **Dependency graph** — visual ordering +4. **Open questions** — anything that needs clarification before implementation begins +5. **Suggested PR strategy** — single PR vs multiple, review order + +## Rules +- Every sub-issue must reference specific files and patterns from the codebase +- No vague specs: "implement the backend" is not a sub-issue +- If you find ambiguity in the story, flag it as an open question rather than guessing +- Check for related existing issues with `gh issue list --search "keyword" --limit 10` +- Consider Fleet's multi-platform nature: does this affect macOS, Windows, Linux, iOS, Android? +- Consider enterprise vs core: does this need license checks? diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md new file mode 100644 index 0000000000..9bd6e20ff0 --- /dev/null +++ b/.claude/skills/test/SKILL.md @@ -0,0 +1,31 @@ +--- +name: test +description: Run tests related to recent changes with appropriate tools and environment variables. Use when asked to "run tests", "test my changes", or "test this". +allowed-tools: Bash(go test *), Bash(MYSQL_TEST*), Bash(MYSQL_TEST=1 *), Bash(MYSQL_TEST=1 REDIS_TEST=1 *), Bash(FLEET_INTEGRATION_TESTS_DISABLE_LOG=1 *), Bash(yarn test*), Bash(npx jest*), Bash(git diff*), Bash(git status*), Read, Grep, Glob +effort: low +--- + +Run tests related to my recent changes. Look at `git diff` and `git diff --cached` to determine which files were modified. + +## Go tests + +For each modified Go package, run the tests with appropriate env vars: +- If the package is under `server/datastore/mysql`: use `MYSQL_TEST=1` +- If the package is under `server/service`: use `MYSQL_TEST=1 REDIS_TEST=1` +- Otherwise: run without special env vars + +## Frontend tests + +If any files under `frontend/` were modified, run the relevant frontend tests: +- Find test files matching the changed components (e.g., `ComponentName.tests.tsx`) +- Run with: `yarn test --testPathPattern "path/to/changed/component"` +- If many files changed, run the full suite: `yarn test` + +## Choosing what to run + +- If only Go files changed, run Go tests only +- If only frontend files changed, run frontend tests only +- If both changed, run both +- If an argument is provided, use it as a filter: $ARGUMENTS (passed as `-run` for Go or `--testPathPattern` for frontend) + +Show a summary of results: which packages/suites passed, which failed, and any failure details.