diff --git a/CLAUDE.md b/CLAUDE.md index d3d8811c..5618e434 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Read first + +- `docs/GLOSSARY.md` — canonical terms for Pro vs Platform vs Classic, blueprint vs config profile, smart vs static groups, scope vs target, etc. Consult before guessing. +- `docs/solutions/` — categorized postmortems and design-pattern docs (e.g., `conventions/output-flag-matrix-2026-05-08.md`, `design-patterns/cobra-annotations-as-policy-2026-05-11.md`). When starting work in a package, grep `docs/solutions/` for matching `module:` or `tags:` frontmatter. + ## CRITICAL: Credential Input Policy **Never accept credentials (passwords, tokens, client secrets) via CLI flags or stdin.** This prevents exposure in shell history, `ps` output, and CI/CD logs. diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md new file mode 100644 index 00000000..c3c5c312 --- /dev/null +++ b/docs/GLOSSARY.md @@ -0,0 +1,122 @@ +# Glossary + +Canonical terms used in jamf-cli. When user-facing ambiguity affects what +action to take, ask before acting — the same word often means different +things across Pro, Platform, Protect, and School. + +## Product namespaces + +| Term | Meaning | +|------|---------| +| **Jamf Pro** (`pro`) | The flagship MDM/UEM. Surfaces both the modern **UAPI** (`/api/v1/...`) and the legacy **Classic API** (`/JSSResource/...`). Most commands live here. | +| **Jamf Platform** / **Platform Gateway** (`platform`) | The cross-product API surface hosted at `.apigw.jamf.com`. Adds Blueprints, Compliance Benchmarks, Platform Devices/Groups. Platform auth (`auth-method: platform`) also enables Pro API calls — they're proxied through the gateway at `/api/pro/tenant//...` and `/api/proclassic/tenant//...`. | +| **Jamf Protect** (`protect`) | The EDR/security product. GraphQL only; uses `jamfprotect-go-sdk`. No relation to Pro auth — separate credentials, separate `JAMFPROTECT_*` env vars. | +| **Jamf School** (`school`) | The K-12-focused MDM. Separate REST API; uses `jamfschool-go-sdk`. Separate `JAMFSCHOOL_*` env vars. | + +## API surfaces under Pro + +| Term | Meaning | +|------|---------| +| **UAPI** / **modern API** | The newer Pro JSON REST API at `/api/v1/...` (and `/v2`, `/v3`). What new feature work targets. | +| **Classic API** | The legacy XML API at `/JSSResource/...`. Still widely used; many resources have no UAPI equivalent. Routed through `/api/proclassic/tenant/{id}/...` under Platform gateway auth. | +| **JCDS** | Jamf Cloud Distribution Service — file storage for installer packages. Commands: `pro jcds upload`, `pro jcds download`, `pro jcds sync`. | +| **API integration** / **API client** | An OAuth2 client-credentials pairing registered in Pro (Settings > System > API Roles & Clients). Distinct from a generic API token. | + +## Authentication + +| Term | Meaning | +|------|---------| +| **Token auth** (`auth-method: token`) | Pre-existing bearer token in the `Authorization: Bearer ...` header. Used for legacy basic-auth bootstraps and short-lived UAPI sessions. | +| **OAuth2** (`auth-method: oauth2`) | Client-credentials flow against the instance's `/api/oauth/token`. The standard for modern Pro automation. | +| **Platform** (`auth-method: platform`) | Client-credentials flow against the gateway's `/auth/token`. Requires `--tenant-id`. Enables both Platform API commands AND Pro API commands routed through the gateway. | +| **`keychain:` / `env:` / `file:` references** | Config-file prefixes for resolving secrets at runtime. Bare values are never stored in `config.yaml` — they're moved to keychain on profile creation. | + +## Configuration delivery + +These words look interchangeable but aren't. Picking the wrong one changes +which API you call and which device-side mechanism applies the config. + +| Term | Meaning | +|------|---------| +| **Configuration profile** / **config profile** / **mobileconfig** | An XML `.mobileconfig` payload (legacy Apple format). Delivered via the Pro UAPI or Classic API. Traditional imperative MDM. | +| **Declarative Device Management** / **DDM** | Per Apple, an **additive** layer on top of the MDM protocol. Devices proactively and autonomously apply management settings and report state changes asynchronously via the **status channel**. Not a replacement for MDM — coexists with it on the same enrollment. | +| **Declaration** | A single DDM payload (e.g., `com.apple.configuration.passcode.settings`, `com.apple.configuration.softwareupdate.settings`). The device pulls and applies declarations on its own schedule rather than waiting for an MDM push. | +| **Status channel** | The DDM-specific communication channel devices use to proactively report state changes back to Jamf Pro/School. Some inventory attributes subscribe to this channel and update without an explicit inventory command. | +| **Blueprint** | A **Jamf Pro** AND **Jamf School** feature that bundles DDM declarations (and increasingly, configuration-profile payloads too) into a single deployable unit. Built on DDM. Scoped to smart groups or static groups. Available in both products via the Platform Gateway API surface — *the API lives in `specs/platform/`, but the user-facing concept is a Pro/School feature, not a separate "Platform product".* | +| **Blueprint component** | A single declaration or supported configuration-profile payload inside a blueprint. Newer Apple payloads (AirPrint, Restrictions, Lock Screen Message, etc.) can be added as components alongside DDM declarations. | +| **Legacy-to-DDM conversion** | `import-profile` auto-converts compatible mobileconfig payloads into native DDM components in a blueprint. `--legacy` opts out. Unsupported payloads are filtered unless `--include-unsupported` is set. | + +## Groups and scope + +| Term | Meaning | +|------|---------| +| **Smart group** | A dynamic group whose membership is computed from criteria (e.g., "all Macs running macOS 14"). Pro has `smart-computer-groups`, `mobile-device-smart-groups`. Membership refreshes on inventory updates. | +| **Static group** | A manually-curated list of devices. Pro has `static-computer-groups`, `mobile-device-static-groups`. Add/remove by ID. | +| **Scope** | In Jamf Pro, **scope is the unified concept** for "which computers/mobile devices/users receive a remote management task." It comprises three sub-functions: **targets**, **limitations**, and **exclusions** (see below). Classic API exposes scope as `` XML; UAPI as a `scope` JSON object. Scope can be based on individual devices/users, groups, departments, buildings, directory groups, network segments, classes, or iBeacon regions — the available items vary per task type. **Scope cannot be based on personally owned devices.** | +| **Target** (scope sub-function) | The initial pool of intended recipients for a management task. **Required** to deploy any task. *Not* a separate Platform-only concept — targets are part of Pro scope. | +| **Limitation** (scope sub-function) | Optional filter that reduces the target pool to a specific subsection. Requires an initial target. | +| **Exclusion** (scope sub-function) | Optional filter that omits devices/users from the target pool. **Exclusions always override targets and limitations.** | +| **Extension Attribute** / **EA** | A custom inventory field defined by a Pro admin (e.g., "Is FileVault enabled?"). Computers and mobile devices have separate EA registries. Compliance Benchmarks auto-generate per-benchmark EAs (`[Benchmark Name] - Failed Result List`). | + +## MDM mechanics + +| Term | Meaning | +|------|---------| +| **MDM command** | An imperative push to a device (e.g., "Send blank push", "Update inventory", "Wipe computer", "Enable/Disable Bluetooth", "OS update — download & install"). Pro tracks pass/fail state per command; flush stuck or failed ones with `pro computers flush-commands`. | +| **DDM** (vs MDM command) | Additive layer — devices pull declarations and report state autonomously via the status channel. Same enrollment runs both; pick the right tool: imperative one-shot action → MDM command; continuously-enforced configuration state → declaration. | +| **PreStage enrollment** | (Capital S — that's the Apple/Jamf canonical spelling.) Configuration applied to devices going through Automated Device Enrollment, configured *before* the device activates. `computer-prestages` for macOS, `mobile-device-prestages` for iOS/iPadOS. Settings sync with Apple every two minutes. | +| **Automated Device Enrollment** / **ADE** | Apple's zero-touch enrollment program. **Formerly DEP.** Jamf docs use "Automated Device Enrollment" or "ADE"; DEP is explicitly the old name. Devices purchased through Apple Business Manager / Apple School Manager auto-enroll without user action. | +| **Account-driven Device Enrollment** | (macOS 14+) Alternative to PreStage where users sign in with a Managed Apple Account in System Settings to initiate enrollment. No URL link required. | + +## Compliance + +| Term | Meaning | +|------|---------| +| **Compliance Benchmarks** | A **Jamf Pro** feature built on the **macOS Security Compliance Project (mSCP)** framework. Supports industry standards (CIS macOS Benchmarks, etc.) for aligning Mac fleet security posture with recognized practices. | +| **Benchmark** | A specific compliance configuration created from a template (e.g., CIS Level 1). Each benchmark has its own name and gets a Jamf-Pro-auto-generated smart group `[Name] - Compliant` + extension attribute `[Name] - Failed Result List`. | +| **Benchmark template** | The mSCP-derived starting point used when creating a benchmark — e.g., CIS L1, CIS L2, BIO2 (Government Information Security Baseline 2), NLMAPGOV. | +| **Rule** | One individual check within a benchmark (e.g., "FileVault must be enabled"). Vendor-defined; updates flow from mSCP. Some rules accept **ODVs** (Organization-Defined Values) for customization. | +| **Enforcement type** | Per-benchmark setting controlling whether failures auto-remediate. Jamf recommends starting with **Monitor only** to observe before enforcing. | +| **Baseline** *(Platform API term)* | The CLI subcommand and SDK term for the customer-editable YAML config that selects rules within a benchmark version. The Jamf Pro UI doesn't use "baseline" — it talks about "rules" and "ODVs" directly. This is a naming-gap between the underlying Platform API (`compliance-benchmarks baselines`) and the user-facing UI. | + +## Commands and aliases + +| Term | Meaning | +|------|---------| +| **Generated command** | A command emitted by `make generate` into `internal/commands/{pro,platform}/generated/`. **Never hand-edit these.** Fix the template in `generator/parser/generator.go` instead. | +| **Hand-written command** | A command in `internal/commands/pro_*.go`, `protect_*.go`, `school_*.go`. Owns business logic (upsert, apply, clone, etc.) — wraps generated commands. | +| **`apply`** | Upsert (create-or-update) by name. Hand-written; available on most resources. | +| **`overview`** | A product-level dashboard command. `pro overview` makes ~37 parallel API calls; `protect overview` makes ~14. | +| **Alias** | A short alternate command name, registered in `aliases.go`. E.g., `comp` → `computers`, `bp` → `blueprints`. | + +## Implementation conventions + +| Term | Meaning | +|------|---------| +| **`pro` / `protect` / `school` / `platform`** as command-tree parents | Each product has a bridge file (`pro.go`, `protect.go`, etc.) that wires its subcommands. New commands land under the right bridge. | +| **`cliCtx` / `registry.CLIContext`** | The shared infrastructure handle passed to every command — HTTPClient, AuthProvider, Output formatter, optional PlatformSDKClient/Protect/School clients. | +| **Cobra annotations (`lint:*`, `jamf:*`, `mcp:*`)** | Policy metadata attached to commands. `lint:keep-flag` suppresses the dead-code linter; future namespaces will drive MCP exposure, exit-code policy, destructive-command gating. Read the cobra `Annotations` map, not magic strings in code. | +| **Provenance** | The spec file paths + SHA256 hashes baked into the generated package at `make generate` time. Surfaced via `jamf-cli version -v`. Lets users diagnose "which spec version is this CLI generated from?" without git archaeology. | + +## When to ask + +- **"scope"** — could mean (a) the full Pro scope object (targets + limitations + exclusions), (b) just the "targets" sub-function casually called "scope", or (c) Platform's scope field on a blueprint. Different field names in code. +- **"target"** — almost always means a Pro scope sub-function. Don't assume it's a separate Platform concept. +- **"profile"** — could mean (a) a config-file profile in `~/.config/jamf-cli/`, (b) a mobileconfig configuration profile, (c) a Jamf-managed enrollment profile (MDM profile installed at enrollment), (d) a PreStage enrollment payload. Ask which. +- **"group"** — smart vs static, computer vs mobile — four combinations. Ask if unclear. +- **"policy"** — Pro has policies (`/policies` Classic), Protect has plans (sometimes informally called "policies"), Platform doesn't have either. Confirm the product. +- **"command"** — could mean a CLI subcommand (`jamf-cli pro computers list`) or an MDM command (a `DeviceLock` push). Context usually disambiguates, but if not, ask. +- **"benchmark" vs "baseline"** — in the Jamf Pro UI, the unit is a *benchmark*, with *rules* and *ODVs*. In the CLI/Platform API, *baseline* shows up as a subcommand because that's the API resource name. They refer to overlapping but not identical things — ask whether the user means the UI concept or the API resource. + +## Sources + +The product-terminology entries above were cross-checked against Jamf Product +Documentation (`learn.jamf.com`) via Ask Jamf — particularly the Jamf Pro +Documentation, Jamf Pro Blueprints Configuration Guide, Compliance Benchmarks +Configuration Guide, and Jamf 100 Course Lesson 17 (Device Scope). When +documentation and CLI behavior diverge (e.g., the `baseline` naming gap), +both are noted. + +If you find an entry that contradicts current `learn.jamf.com` content, fix +the entry — these are point-in-time captures of vocabulary that can drift +with product renames (DEP → ADE, Azure ID → Microsoft Entra ID, etc.). diff --git a/docs/solutions/README.md b/docs/solutions/README.md new file mode 100644 index 00000000..039412f2 --- /dev/null +++ b/docs/solutions/README.md @@ -0,0 +1,91 @@ +# Solutions + +Documented solutions to past problems — bugs, design patterns, conventions — +that future contributors (human or AI) should learn from rather than rediscover. + +Inspired by [cli-printing-press](https://github.com/mvanhorn/cli-printing-press)'s +`docs/solutions/` archive. The shape is intentionally close to theirs so the +authoring habit transfers cleanly. + +## When to write a solution doc + +Write one when **any** of these apply: + +- You fixed a class of bug, not a single instance. The fix is generalizable, but + the path to discovering it is non-obvious. +- You introduced (or refactored to) a pattern that other parts of the codebase + should adopt. Documenting it now beats explaining it in five future code + reviews. +- A convention was decided that isn't enforceable by lint or tests. The doc + is the enforcement mechanism — reviewers point to it. +- A debugging session took >30 minutes and the next person hitting the same + symptom should be able to short-circuit it. + +Don't write one for: + +- One-off bugs in a single file with no broader lesson. The commit message is + enough. +- Conventions already covered in `CLAUDE.md` or `docs/GLOSSARY.md`. +- Style preferences without a load-bearing reason behind them. + +## Directories + +Group by problem type: + +- `best-practices/` — how to do something well, when more than one way exists +- `conventions/` — rules the codebase follows that aren't enforceable by tooling +- `design-patterns/` — reusable structural patterns with named application sites +- `logic-errors/` — bug classes worth remembering (root cause + correct fix) +- `security-issues/` — anything where the wrong fix has a security cost + +## File format + +Each solution is a single Markdown file. Filename pattern: +`-.md`. The date is the resolution date, not the +discovery date. + +Required YAML frontmatter: + +```yaml +--- +title: "One-line title describing the rule, not the bug" +date: 2026-05-08 +category: conventions # one of the directory names above +module: # e.g., "internal/commands", "generator", "internal/output" +problem_type: convention # design_pattern | convention | bug | best_practice | security +severity: medium # low | medium | high +applies_when: + - "concrete situation 1" + - "concrete situation 2" +tags: [keyword1, keyword2, keyword3] +--- +``` + +Body sections: + +```markdown +## Context + +What happened, what was confusing, what didn't work. Link to PRs, issues, +or commits. + +## Guidance + +The rule itself — actionable, in imperative voice. If there's a code shape, +show the shape. If there's a checklist, list it. + +## Why this beats the alternative (optional) + +Only when "just do X" isn't obvious and the cost of the wrong path is real. +``` + +## Reading these + +Future contributors (human and AI) should `grep` this directory by tag or +keyword when implementing or debugging in a documented area. The frontmatter +is structured so it's easy to search across. + +Future AI sessions: when starting work in a package, scan +`docs/solutions/*/*.md` for matching `module:` or `tags:`. If a solution +covers the area you're touching, follow its guidance rather than rediscovering +the same bug. diff --git a/docs/solutions/conventions/output-flag-matrix-2026-05-08.md b/docs/solutions/conventions/output-flag-matrix-2026-05-08.md new file mode 100644 index 00000000..da1cec0b --- /dev/null +++ b/docs/solutions/conventions/output-flag-matrix-2026-05-08.md @@ -0,0 +1,100 @@ +--- +title: "Every new command must honor the global output/flag matrix" +date: 2026-05-08 +category: conventions +module: internal/commands +problem_type: convention +severity: medium +applies_when: + - "Adding a new top-level command or subcommand" + - "Adding a new code path that writes to stdout or stderr (status messages, hints, spinners)" + - "Adding a verbose mode to an existing command" +tags: + - output + - flags + - no-color + - quiet + - structured-output + - cross-flag-honoring +--- + +# Every new command must honor the global output/flag matrix + +## Context + +Two follow-up fix PRs shipped immediately after the introduction of two new +commands, both because the original work didn't exercise the cross-flag matrix: + +- **PR #194 (spinner+NO_COLOR):** The Pro/Protect/School/Platform spinners + emit `\r` + `\033[K` ANSI to stderr. The wrapping decision consulted + `--quiet` and `-v` but not `--no-color` or the `NO_COLOR` env. Users who set + `NO_COLOR=1` expect ANSI noise to stop, period — that's the + [no-color.org](https://no-color.org) contract. + +- **PR #195 (`version -v` ignoring `-o json/yaml`):** The verbose `version -v` + path hardcoded text output. Passing `-o json` silently still printed text. + Worst of both worlds: the user asked for structured output and got prose. + Fixed by routing the verbose path through the formatter the same way + `doctor` does, with a partitioned `specSources` shape. + +Both fixes are small and obvious in hindsight. The cost was a follow-up PR, +review cycle, and merge for each. + +## Guidance + +For **every** new command or new output path, exercise this matrix before +considering the work complete: + +| Flag / env | Expected behavior | +|------------|-------------------| +| (default) | Human-friendly output | +| `-o json` | Structured JSON; ANSI off; ready to pipe | +| `-o yaml` | Structured YAML | +| `-o csv` | Tabular, where the data is row-shaped | +| `-o table` | Default for lists | +| `--quiet` / `-q` | Suppress advisory output (hints, spinners, progress) | +| `--no-color` or `NO_COLOR=1` | No ANSI escapes anywhere — stdout AND stderr | +| `--out-file ` | Output goes to file; also disable color | +| `--field ` | Single-field extraction | +| `--select ` | Multi-field projection | +| `--compact` | Drop arrays and nested objects | + +### Specific rules + +1. **Anything writing ANSI to stderr** (spinners, status updates, hints) must + consult `noColor` (or `os.Getenv("NO_COLOR") != ""`) alongside `quiet` and + `verboseLevel`. See `shouldShowSpinner()` in `internal/commands/root.go` for + the canonical helper. + +2. **Verbose/structured commands** (commands with a `-v` mode like `doctor` + and `version`) must route their verbose output through `cliCtx.Output.PrintRaw(...)` + when `outputFmt` is `json` or `yaml`, not through hand-written `fmt.Fprintf` + on `os.Stdout`. The formatter knows how to honor the global flags; hand-print + paths don't. + +3. **Marshal structs; don't hand-format prose** when the output is structured. + Define an explicit response struct with JSON tags. Let the formatter handle + indentation, projection, and filtering. + +4. **Test what you ship.** Before merge, run the new command with at least: + ```bash + bin/jamf-cli # default + bin/jamf-cli -o json + bin/jamf-cli -o yaml + bin/jamf-cli --quiet + bin/jamf-cli --no-color + bin/jamf-cli --out-file /tmp/out.txt + ``` + + For commands producing lists, also test `--compact`, `--select`, and + `--field`. + +## Why this beats the alternative + +The alternative is "land the command, fix the flag interactions in follow-up." +That's what happened in #194 and #195. Each follow-up costs a PR, a review, +and a merge — and exposes users to surprising behavior between releases. + +The matrix is short enough to walk every time. A `make smoke-cli` target that +runs each new command through the matrix would automate this; until then, the +checklist above is the discipline. diff --git a/docs/solutions/design-patterns/cobra-annotations-as-policy-2026-05-11.md b/docs/solutions/design-patterns/cobra-annotations-as-policy-2026-05-11.md new file mode 100644 index 00000000..a8e50bed --- /dev/null +++ b/docs/solutions/design-patterns/cobra-annotations-as-policy-2026-05-11.md @@ -0,0 +1,127 @@ +--- +title: "Cobra annotations as policy contract for cross-cutting concerns" +date: 2026-05-11 +category: design-patterns +module: internal/commands +problem_type: design_pattern +severity: low +applies_when: + - "Adding a structural verifier (lint, audit, scorecard) that needs per-command metadata" + - "Designing MCP exposure rules (read-only, destructive, hidden) for commands" + - "Marking commands that intentionally use non-zero exit codes for success-by-policy" + - "Adding a destructive-command confirmation gate that needs an allowlist" +tags: + - cobra + - annotations + - policy + - lint + - mcp + - destructive-commands + - structural-verification +--- + +# Cobra annotations as policy contract for cross-cutting concerns + +## Context + +When jamf-cli's dead-code lint (PR #198) needed an allowlist mechanism for +intentional flag retentions, the choice was between: + +- Comment-based markers (`//lint:keep` above declarations) +- A separate `.linterignore` file with patterns +- Cobra command annotations (`Annotations: map[string]string{"lint:keep-flag": "name1,name2"}`) + +We picked Cobra annotations — and the deciding factor wasn't this single +linter. It was the recognition that every future structural verifier +(anti-reimplementation, MCP-surface-parity, naming-consistency, destructive- +command gating, exit-code policy) will need similar per-command metadata. A +single annotation namespace serves them all. + +The cli-printing-press project ([reference](https://github.com/mvanhorn/cli-printing-press)) +uses the same convention for `pp:endpoint`, `mcp:hidden`, `mcp:read-only`, +`pp:typed-exit-codes`, `pp:novel-static-reference`. Each annotation drives one +or more verifiers without that verifier having to maintain its own allowlist. + +## Guidance + +When you need per-command metadata that a tool (lint, MCP exposure, exit-code +verifier, gate) consumes, **prefer Cobra annotations over string-matching +files, comment magic, or separate registries**. + +### Namespace conventions + +- `lint:*` — for the structural lint suite. Examples: + - `lint:keep-flag: "name1,name2"` — suppress dead-flag finding for these flags + - `lint:keep-func` (future) — suppress dead-func finding for the command's RunE +- `mcp:*` — for the MCP server (when added). Examples: + - `mcp:hidden: "true"` — don't expose this command as an MCP tool + - `mcp:read-only: "true"` — annotate as readOnlyHint when exposed +- `jamf:*` — for jamf-cli policy. Examples: + - `jamf:destructive: "true"` — flag for confirmation gating; also drives MCP destructiveHint + - `jamf:typed-exit-codes: "0,3"` — declares intentional non-zero success codes + +### Reading annotations + +Annotations are `map[string]string` on `*cobra.Command`. Values are bare strings; +encode lists as comma-separated. Readers must handle missing keys (zero value = +no policy applied): + +```go +keep, ok := cmd.Annotations["lint:keep-flag"] +if !ok { + return nil // no opt-outs declared +} +allowed := strings.Split(keep, ",") +``` + +### Writing annotations + +Declare them inline at command construction so the policy is visible next to +the command definition, not in a registry elsewhere: + +```go +cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a computer by ID", + Annotations: map[string]string{ + "jamf:destructive": "true", + "jamf:typed-exit-codes": "0,4", // 4 = not found, treated as success + }, + RunE: func(cmd *cobra.Command, args []string) error { ... }, +} +``` + +### When the static-analysis tool can't read the annotation + +AST readers must handle the case where `Annotations` is set dynamically +(loops, conditionals, function returns). The dead-code lint walks +`*ast.CompositeLit` for the literal `Annotations: map[string]string{...}` +form. If the annotation is set via `cmd.Annotations[...] = ...` after +construction, or returned from a helper, the linter falls back to no +allowlist for that command. + +Prefer the inline literal form so the policy is statically visible. + +## Why this beats the alternative + +**Vs. `//lint:keep` comments:** Comments are positional and brittle — a +refactor that moves a flag binding away from its comment silently disables +the keep. Annotations bind to the command, which is the right granularity. + +**Vs. `.linterignore` file:** A central file becomes write-only history. No +reviewer remembers to check it. The annotation lives next to the command it +governs, so reviewers see it in the diff. + +**Vs. per-tool allowlists in each verifier:** Three verifiers with three +allowlists is three places to forget to update when a command moves. One +annotation namespace, all verifiers read it. + +**Vs. magic strings:** A verifier that string-matches `// HACK keep this` is +indistinguishable from "this comment text accidentally matched." Annotations +are structured data with a defined schema. + +## Related + +- `scripts/lint-dead-code/scan.go` — first consumer of the `lint:*` namespace +- Future structural verifiers should add their own namespace under `lint:*`, + `mcp:*`, or `jamf:*` rather than introducing a new mechanism