{"owner":"snyk","repo":"cli","hasSkills":true,"totalSkillsCount":6,"totalTokensCount":13038,"categories":["root-instruction","claude-rule","mcp-config","marketplace","plugin-manifest","cursor-rule"],"hasMcp":true,"mcpConfig":{"mcpServers":{"cli":{"command":"npx","args":["-y","@modelcontextprotocol/server-cli"]}}},"found":["AGENTS.md","CLAUDE.md",".cursor/mcp.json",".claude-plugin/marketplace.json",".claude-plugin/plugin.json",".cursorrules (ankra-cli.mdc)"],"skills":{"AGENTS.md":"# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n**Scanning for vulnerabilities?** Use agent mode: `snyk agent test --experimental` (experimental, subject to change). It runs Open Source, Code, and Secrets in one pass and returns compact, token-efficient output built for agents (`snyk agent --help`).\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Environment variables\n\nThe config reads only the variables allowlisted in `cliv2/pkg/core/main.go`:\n\n```go\nconfiguration.WithSupportedEnvVars(\"NODE_EXTRA_CA_CERTS\"),\nconfiguration.WithSupportedEnvVarPrefixes(\"snyk_\", \"internal_\", \"test_\"),\n```\n\nAnything else is invisible. `config.GetString(\"MY_VAR\")` returns `\"\"` however the shell is set. Names an external convention forces on us are added one at a time by full name.\n\nPick the prefix by who reads the value, not who sets it.\n\n| Prefix           | Read by                                          | Contract                                          |\n| ---------------- | ------------------------------------------------ | ------------------------------------------------- |\n| `SNYK_`          | Users, CI, IDEs                                  | Public and documented. Renaming one breaks users. |\n| `INTERNAL_`      | The Go CLI, through the config                   | None. Change it whenever.                         |\n| `SNYK_INTERNAL_` | The legacy TypeScript CLI, through `process.env` | None.                                             |\n| `TEST_`          | Tests                                            | None. Never read one from production code.        |\n\n- Read with `config.GetString`, never `os.Getenv`, so flags, config files, alternative keys and caching still apply.\n- A wrapper or an IDE setting the variable does not make it public. `INTERNAL_SNYK_CLIENT_MACHINE_ID` (`cliv2/pkg/core/instrumentation.go`) is set by another Snyk process and read by the CLI.\n- To put a public name on an internal key, add an alternative key: `config.AddAlternativeKeys(cliv2.ConfigKeyRequestConcurrency, []string{\"snyk_request_concurrency\"})`. Code keeps reading `internal_request_concurrency`, users set `SNYK_REQUEST_CONCURRENCY`.\n- The legacy CLI is a child process with no config engine, so values reach it only if you pass them: a constant in `cliv2/internal/constants/constants.go`, assigned in `fillEnvironmentFromConfig` (`cliv2/internal/cliv2/cliv2.go`). See `SNYK_INTERNAL_ORGID`.\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n### Tests must see what customers see\n\nWhen an acceptance test fails, find out why before changing anything. Users often hit the same thing: a feature that is off by default, a flag they would have to set, a code path the shipped binary never takes. Report it. Don't hide it by adding env vars or feature flags, stubbing the thing under test, loosening assertions, or skipping the test. If you think the setup is legitimate, ask first and leave a comment explaining it.\nCI builds carry a `-dev` or `-preview` version, and GAF turns preview features on for those. Do not set `INTERNAL_PREVIEW_FEATURES_ENABLED: 'true'` when testing customer-facing behaviour.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nBefore creating or editing a PR, read `.github/PULL_REQUEST_TEMPLATE.md` and fill its relevant sections, including **\"What's the product update that needs to be communicated to CLI users?\"**, manual testing instructions, and a risk assessment. This applies to dependency-only bumps too: describe the user-visible fix brought in by the dependency, link the upstream change and regression coverage, and state any remaining limitations. Check previous reviewer feedback before asking for review. Keep the description within 200 words (250 hard ceiling), and distinguish completed validation from pending or blocked checks.\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n### Handling `go.sum` and private modules\n\nAlways use Go tooling (`go get`, `go mod tidy`, `make tidy`) to update `go.mod` and `go.sum`. Never edit `go.sum` manually — its entries must be in lexicographic order, and manual edits easily break sorting.\n\nWhen private modules (e.g. `ambient-canary`) block `go mod tidy`, use `go mod tidy -e` to tolerate fetch errors while still maintaining correct formatting and sort order. If manual `go.sum` editing is truly unavoidable (last resort only), entries must be inserted in sorted position — never appended to the end of the file. Verify with `sort -c go.sum` after any manual edit.\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n","CLAUDE.md":"# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n**Scanning for vulnerabilities?** Use agent mode: `snyk agent test --experimental` (experimental, subject to change). It runs Open Source, Code, and Secrets in one pass and returns compact, token-efficient output built for agents (`snyk agent --help`).\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Environment variables\n\nThe config reads only the variables allowlisted in `cliv2/pkg/core/main.go`:\n\n```go\nconfiguration.WithSupportedEnvVars(\"NODE_EXTRA_CA_CERTS\"),\nconfiguration.WithSupportedEnvVarPrefixes(\"snyk_\", \"internal_\", \"test_\"),\n```\n\nAnything else is invisible. `config.GetString(\"MY_VAR\")` returns `\"\"` however the shell is set. Names an external convention forces on us are added one at a time by full name.\n\nPick the prefix by who reads the value, not who sets it.\n\n| Prefix           | Read by                                          | Contract                                          |\n| ---------------- | ------------------------------------------------ | ------------------------------------------------- |\n| `SNYK_`          | Users, CI, IDEs                                  | Public and documented. Renaming one breaks users. |\n| `INTERNAL_`      | The Go CLI, through the config                   | None. Change it whenever.                         |\n| `SNYK_INTERNAL_` | The legacy TypeScript CLI, through `process.env` | None.                                             |\n| `TEST_`          | Tests                                            | None. Never read one from production code.        |\n\n- Read with `config.GetString`, never `os.Getenv`, so flags, config files, alternative keys and caching still apply.\n- A wrapper or an IDE setting the variable does not make it public. `INTERNAL_SNYK_CLIENT_MACHINE_ID` (`cliv2/pkg/core/instrumentation.go`) is set by another Snyk process and read by the CLI.\n- To put a public name on an internal key, add an alternative key: `config.AddAlternativeKeys(cliv2.ConfigKeyRequestConcurrency, []string{\"snyk_request_concurrency\"})`. Code keeps reading `internal_request_concurrency`, users set `SNYK_REQUEST_CONCURRENCY`.\n- The legacy CLI is a child process with no config engine, so values reach it only if you pass them: a constant in `cliv2/internal/constants/constants.go`, assigned in `fillEnvironmentFromConfig` (`cliv2/internal/cliv2/cliv2.go`). See `SNYK_INTERNAL_ORGID`.\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n### Tests must see what customers see\n\nWhen an acceptance test fails, find out why before changing anything. Users often hit the same thing: a feature that is off by default, a flag they would have to set, a code path the shipped binary never takes. Report it. Don't hide it by adding env vars or feature flags, stubbing the thing under test, loosening assertions, or skipping the test. If you think the setup is legitimate, ask first and leave a comment explaining it.\nCI builds carry a `-dev` or `-preview` version, and GAF turns preview features on for those. Do not set `INTERNAL_PREVIEW_FEATURES_ENABLED: 'true'` when testing customer-facing behaviour.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nBefore creating or editing a PR, read `.github/PULL_REQUEST_TEMPLATE.md` and fill its relevant sections, including **\"What's the product update that needs to be communicated to CLI users?\"**, manual testing instructions, and a risk assessment. This applies to dependency-only bumps too: describe the user-visible fix brought in by the dependency, link the upstream change and regression coverage, and state any remaining limitations. Check previous reviewer feedback before asking for review. Keep the description within 200 words (250 hard ceiling), and distinguish completed validation from pending or blocked checks.\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n### Handling `go.sum` and private modules\n\nAlways use Go tooling (`go get`, `go mod tidy`, `make tidy`) to update `go.mod` and `go.sum`. Never edit `go.sum` manually — its entries must be in lexicographic order, and manual edits easily break sorting.\n\nWhen private modules (e.g. `ambient-canary`) block `go mod tidy`, use `go mod tidy -e` to tolerate fetch errors while still maintaining correct formatting and sort order. If manual `go.sum` editing is truly unavoidable (last resort only), entries must be inserted in sorted position — never appended to the end of the file. Verify with `sort -c go.sum` after any manual edit.\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n",".cursor/mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.cursor/mcp.json)",".claude-plugin/marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/marketplace.json)",".claude-plugin/plugin.json":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/plugin.json)",".cursorrules (ankra-cli.mdc)":"---\ndescription: \"Ankra CLI rules and best practices for managing Kubernetes clusters via the Ankra platform\"\nglobs: **/*.sh, **/*.yaml, **/*.yml, Makefile, **/Makefile, **/*.md\nalwaysApply: false\n---\n# Ankra CLI Best Practices\n\nAnkra is a Kubernetes cluster management platform. Its CLI (`ankra`) lets you manage organisations, clusters, add-ons, stacks, Helm releases, credentials, and tokens from the terminal.\n\n## Authentication & Configuration\n\n- Authenticate first: `ankra login`\n- Default config lives at `~/.ankra.yaml`; override with `--config <path>`\n- Override the API base URL with `--base-url` or `ANKRA_BASE_URL`\n- Supply an API token via `--token` or `ANKRA_API_TOKEN` (prefer env var in CI)\n- Logout cleanly: `ankra logout`\n\n```yaml\n# ~/.ankra.yaml example\nbase_url: https://api.ankra.io\ntoken: ${ANKRA_API_TOKEN}\n```\n\n## Organisation Management\n\n```bash\nankra org list                          # List accessible organisations\nankra org switch <org-name>             # Switch active organisation\nankra org create <name>                 # Create a new organisation\nankra org invite <email>                # Invite a user to the current org\nankra org members                       # List members of the current org\n```\n\n## Cluster Lifecycle\n\n```bash\nankra cluster list                      # List all clusters in the org\nankra cluster select <name>             # Set the active cluster context\nankra cluster info                      # Show details about the active cluster\nankra cluster provision                 # Provision a new cluster\nankra cluster deprovision               # Deprovision (destroy) the active cluster\nankra cluster reconcile                 # Reconcile cluster to desired state\nankra delete cluster <name>             # Delete a named cluster\n```\n\n- Always run `ankra cluster info` before destructive operations\n- Use `ankra cluster reconcile` after config changes before assuming state is correct\n\n## Add-ons\n\n```bash\nankra cluster addons list               # List installed add-ons\nankra cluster addons available          # List available (installable) add-ons\nankra cluster addons settings <addon>   # Show current settings for an add-on\nankra cluster addons update <addon>     # Update add-on configuration\nankra cluster addons uninstall <addon>  # Uninstall an add-on\n```\n\n## Stacks (Application Stacks)\n\n```bash\nankra cluster stacks list               # List stacks in the cluster\nankra cluster stacks create <name>      # Create a new stack\nankra cluster stacks delete <name>      # Delete a stack\nankra cluster stacks rename <old> <new> # Rename a stack\nankra cluster stacks clone <src> <dst>  # Clone a stack\nankra cluster stacks history <name>     # View stack change history\n```\n\n- Prefer `clone` over `create` when replicating an existing working stack\n- Check `history` before rolling back to understand what changed\n\n## Manifests & Operations\n\n```bash\nankra cluster manifests list            # List deployed manifests\nankra cluster operations list           # List in-progress or recent operations\nankra cluster operations cancel <id>    # Cancel a running operation\n```\n\n## Cluster Agent\n\n```bash\nankra cluster agent status              # Check agent health\nankra cluster agent token               # Retrieve agent bootstrap token\nankra cluster agent upgrade             # Upgrade the in-cluster agent\n```\n\n- Run `agent status` if cluster commands seem unresponsive\n- Rotate the agent token periodically for security\n\n## Helm\n\n```bash\nankra cluster helm releases             # List Helm releases in the cluster\nankra cluster helm uninstall <release>  # Uninstall a Helm release\nankra helm registries                   # List configured Helm registries\nankra helm credentials                  # List Helm registry credentials\n```\n\n## Hetzner Cloud Clusters\n\n```bash\nankra cluster hetzner create            # Create a Hetzner-backed cluster\nankra cluster hetzner scale             # Scale the cluster node count\nankra cluster hetzner upgrade           # Upgrade Kubernetes version\nankra cluster hetzner workers           # Manage worker node groups\nankra cluster hetzner node-group        # Node group management\n```\n\n## Charts\n\n```bash\nankra charts list                       # List available charts\nankra charts search <term>              # Search charts\nankra charts info <chart>               # Detailed chart information\n```\n\n## Credentials\n\n```bash\nankra credentials list                              # List stored credentials\nankra credentials get <name>                        # Retrieve a credential\nankra credentials validate <name>                   # Validate a credential\nankra credentials delete <name>                     # Delete a credential\nankra credentials hetzner --name <n> --token <t>   # Store Hetzner API token\nankra credentials ovh ...                           # Store OVH credentials\nankra credentials upcloud ...                       # Store UpCloud credentials\n```\n\n- Never commit credentials to source control; use `ANKRA_API_TOKEN` env var\n- Validate credentials immediately after creation: `ankra credentials validate <name>`\n\n## Tokens (API Tokens)\n\n```bash\nankra tokens list                       # List API tokens\nankra tokens create --name <name>       # Create a new API token\nankra tokens revoke <id>                # Revoke a token (keeps record)\nankra tokens delete <id>                # Permanently delete a token\n```\n\n## AI Troubleshooting\n\n```bash\nankra chat                              # Start AI-powered troubleshooting session\n```\n\nUse `ankra chat` when facing ambiguous errors — it provides contextual guidance.\n\n## Global Flags\n\n| Flag | Env Var | Description |\n|------|---------|-------------|\n| `--base-url <url>` | `ANKRA_BASE_URL` | Override API base URL |\n| `--token <token>` | `ANKRA_API_TOKEN` | API authentication token |\n| `--config <path>` | — | Path to config file (default `~/.ankra.yaml`) |\n\n## Scripting & Automation Tips\n\n- Export `ANKRA_API_TOKEN` in CI pipelines; never hard-code tokens\n- Use `--token` for one-off scripts where env var injection is not possible\n- Chain `ankra cluster select` before cluster-scoped commands in scripts\n- Pipe output through `jq` for structured processing where JSON output is available\n- Use `ankra cluster operations list` in CI to poll for async operation completion\n- Add `set -euo pipefail` to shell scripts using the Ankra CLI\n\n## Common Workflows\n\n### Provision and configure a new Hetzner cluster\n\n```bash\nankra login\nankra org switch my-org\nankra cluster hetzner create\nankra cluster select my-new-cluster\nankra cluster agent status\nankra cluster addons available\nankra cluster addons update some-addon\nankra cluster reconcile\n```\n\n### Deploy a stack from a chart\n\n```bash\nankra cluster select my-cluster\nankra charts search my-app\nankra charts info my-app\nankra cluster stacks create my-stack\nankra cluster manifests list\n```\n\n### Rotate credentials safely\n\n```bash\nankra tokens create --name ci-token-new\n# Update CI secret with new token\nankra tokens revoke <old-token-id>\nankra credentials validate my-cloud-cred\n```\n"},"files":{"AGENTS.md":"# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n**Scanning for vulnerabilities?** Use agent mode: `snyk agent test --experimental` (experimental, subject to change). It runs Open Source, Code, and Secrets in one pass and returns compact, token-efficient output built for agents (`snyk agent --help`).\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Environment variables\n\nThe config reads only the variables allowlisted in `cliv2/pkg/core/main.go`:\n\n```go\nconfiguration.WithSupportedEnvVars(\"NODE_EXTRA_CA_CERTS\"),\nconfiguration.WithSupportedEnvVarPrefixes(\"snyk_\", \"internal_\", \"test_\"),\n```\n\nAnything else is invisible. `config.GetString(\"MY_VAR\")` returns `\"\"` however the shell is set. Names an external convention forces on us are added one at a time by full name.\n\nPick the prefix by who reads the value, not who sets it.\n\n| Prefix           | Read by                                          | Contract                                          |\n| ---------------- | ------------------------------------------------ | ------------------------------------------------- |\n| `SNYK_`          | Users, CI, IDEs                                  | Public and documented. Renaming one breaks users. |\n| `INTERNAL_`      | The Go CLI, through the config                   | None. Change it whenever.                         |\n| `SNYK_INTERNAL_` | The legacy TypeScript CLI, through `process.env` | None.                                             |\n| `TEST_`          | Tests                                            | None. Never read one from production code.        |\n\n- Read with `config.GetString`, never `os.Getenv`, so flags, config files, alternative keys and caching still apply.\n- A wrapper or an IDE setting the variable does not make it public. `INTERNAL_SNYK_CLIENT_MACHINE_ID` (`cliv2/pkg/core/instrumentation.go`) is set by another Snyk process and read by the CLI.\n- To put a public name on an internal key, add an alternative key: `config.AddAlternativeKeys(cliv2.ConfigKeyRequestConcurrency, []string{\"snyk_request_concurrency\"})`. Code keeps reading `internal_request_concurrency`, users set `SNYK_REQUEST_CONCURRENCY`.\n- The legacy CLI is a child process with no config engine, so values reach it only if you pass them: a constant in `cliv2/internal/constants/constants.go`, assigned in `fillEnvironmentFromConfig` (`cliv2/internal/cliv2/cliv2.go`). See `SNYK_INTERNAL_ORGID`.\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n### Tests must see what customers see\n\nWhen an acceptance test fails, find out why before changing anything. Users often hit the same thing: a feature that is off by default, a flag they would have to set, a code path the shipped binary never takes. Report it. Don't hide it by adding env vars or feature flags, stubbing the thing under test, loosening assertions, or skipping the test. If you think the setup is legitimate, ask first and leave a comment explaining it.\nCI builds carry a `-dev` or `-preview` version, and GAF turns preview features on for those. Do not set `INTERNAL_PREVIEW_FEATURES_ENABLED: 'true'` when testing customer-facing behaviour.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nBefore creating or editing a PR, read `.github/PULL_REQUEST_TEMPLATE.md` and fill its relevant sections, including **\"What's the product update that needs to be communicated to CLI users?\"**, manual testing instructions, and a risk assessment. This applies to dependency-only bumps too: describe the user-visible fix brought in by the dependency, link the upstream change and regression coverage, and state any remaining limitations. Check previous reviewer feedback before asking for review. Keep the description within 200 words (250 hard ceiling), and distinguish completed validation from pending or blocked checks.\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n### Handling `go.sum` and private modules\n\nAlways use Go tooling (`go get`, `go mod tidy`, `make tidy`) to update `go.mod` and `go.sum`. Never edit `go.sum` manually — its entries must be in lexicographic order, and manual edits easily break sorting.\n\nWhen private modules (e.g. `ambient-canary`) block `go mod tidy`, use `go mod tidy -e` to tolerate fetch errors while still maintaining correct formatting and sort order. If manual `go.sum` editing is truly unavoidable (last resort only), entries must be inserted in sorted position — never appended to the end of the file. Verify with `sort -c go.sum` after any manual edit.\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n","CLAUDE.md":"# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n**Scanning for vulnerabilities?** Use agent mode: `snyk agent test --experimental` (experimental, subject to change). It runs Open Source, Code, and Secrets in one pass and returns compact, token-efficient output built for agents (`snyk agent --help`).\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Environment variables\n\nThe config reads only the variables allowlisted in `cliv2/pkg/core/main.go`:\n\n```go\nconfiguration.WithSupportedEnvVars(\"NODE_EXTRA_CA_CERTS\"),\nconfiguration.WithSupportedEnvVarPrefixes(\"snyk_\", \"internal_\", \"test_\"),\n```\n\nAnything else is invisible. `config.GetString(\"MY_VAR\")` returns `\"\"` however the shell is set. Names an external convention forces on us are added one at a time by full name.\n\nPick the prefix by who reads the value, not who sets it.\n\n| Prefix           | Read by                                          | Contract                                          |\n| ---------------- | ------------------------------------------------ | ------------------------------------------------- |\n| `SNYK_`          | Users, CI, IDEs                                  | Public and documented. Renaming one breaks users. |\n| `INTERNAL_`      | The Go CLI, through the config                   | None. Change it whenever.                         |\n| `SNYK_INTERNAL_` | The legacy TypeScript CLI, through `process.env` | None.                                             |\n| `TEST_`          | Tests                                            | None. Never read one from production code.        |\n\n- Read with `config.GetString`, never `os.Getenv`, so flags, config files, alternative keys and caching still apply.\n- A wrapper or an IDE setting the variable does not make it public. `INTERNAL_SNYK_CLIENT_MACHINE_ID` (`cliv2/pkg/core/instrumentation.go`) is set by another Snyk process and read by the CLI.\n- To put a public name on an internal key, add an alternative key: `config.AddAlternativeKeys(cliv2.ConfigKeyRequestConcurrency, []string{\"snyk_request_concurrency\"})`. Code keeps reading `internal_request_concurrency`, users set `SNYK_REQUEST_CONCURRENCY`.\n- The legacy CLI is a child process with no config engine, so values reach it only if you pass them: a constant in `cliv2/internal/constants/constants.go`, assigned in `fillEnvironmentFromConfig` (`cliv2/internal/cliv2/cliv2.go`). See `SNYK_INTERNAL_ORGID`.\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n### Tests must see what customers see\n\nWhen an acceptance test fails, find out why before changing anything. Users often hit the same thing: a feature that is off by default, a flag they would have to set, a code path the shipped binary never takes. Report it. Don't hide it by adding env vars or feature flags, stubbing the thing under test, loosening assertions, or skipping the test. If you think the setup is legitimate, ask first and leave a comment explaining it.\nCI builds carry a `-dev` or `-preview` version, and GAF turns preview features on for those. Do not set `INTERNAL_PREVIEW_FEATURES_ENABLED: 'true'` when testing customer-facing behaviour.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nBefore creating or editing a PR, read `.github/PULL_REQUEST_TEMPLATE.md` and fill its relevant sections, including **\"What's the product update that needs to be communicated to CLI users?\"**, manual testing instructions, and a risk assessment. This applies to dependency-only bumps too: describe the user-visible fix brought in by the dependency, link the upstream change and regression coverage, and state any remaining limitations. Check previous reviewer feedback before asking for review. Keep the description within 200 words (250 hard ceiling), and distinguish completed validation from pending or blocked checks.\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n### Handling `go.sum` and private modules\n\nAlways use Go tooling (`go get`, `go mod tidy`, `make tidy`) to update `go.mod` and `go.sum`. Never edit `go.sum` manually — its entries must be in lexicographic order, and manual edits easily break sorting.\n\nWhen private modules (e.g. `ambient-canary`) block `go mod tidy`, use `go mod tidy -e` to tolerate fetch errors while still maintaining correct formatting and sort order. If manual `go.sum` editing is truly unavoidable (last resort only), entries must be inserted in sorted position — never appended to the end of the file. Verify with `sort -c go.sum` after any manual edit.\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n",".cursor/mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.cursor/mcp.json)",".claude-plugin/marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/marketplace.json)",".claude-plugin/plugin.json":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/plugin.json)",".cursorrules (ankra-cli.mdc)":"---\ndescription: \"Ankra CLI rules and best practices for managing Kubernetes clusters via the Ankra platform\"\nglobs: **/*.sh, **/*.yaml, **/*.yml, Makefile, **/Makefile, **/*.md\nalwaysApply: false\n---\n# Ankra CLI Best Practices\n\nAnkra is a Kubernetes cluster management platform. Its CLI (`ankra`) lets you manage organisations, clusters, add-ons, stacks, Helm releases, credentials, and tokens from the terminal.\n\n## Authentication & Configuration\n\n- Authenticate first: `ankra login`\n- Default config lives at `~/.ankra.yaml`; override with `--config <path>`\n- Override the API base URL with `--base-url` or `ANKRA_BASE_URL`\n- Supply an API token via `--token` or `ANKRA_API_TOKEN` (prefer env var in CI)\n- Logout cleanly: `ankra logout`\n\n```yaml\n# ~/.ankra.yaml example\nbase_url: https://api.ankra.io\ntoken: ${ANKRA_API_TOKEN}\n```\n\n## Organisation Management\n\n```bash\nankra org list                          # List accessible organisations\nankra org switch <org-name>             # Switch active organisation\nankra org create <name>                 # Create a new organisation\nankra org invite <email>                # Invite a user to the current org\nankra org members                       # List members of the current org\n```\n\n## Cluster Lifecycle\n\n```bash\nankra cluster list                      # List all clusters in the org\nankra cluster select <name>             # Set the active cluster context\nankra cluster info                      # Show details about the active cluster\nankra cluster provision                 # Provision a new cluster\nankra cluster deprovision               # Deprovision (destroy) the active cluster\nankra cluster reconcile                 # Reconcile cluster to desired state\nankra delete cluster <name>             # Delete a named cluster\n```\n\n- Always run `ankra cluster info` before destructive operations\n- Use `ankra cluster reconcile` after config changes before assuming state is correct\n\n## Add-ons\n\n```bash\nankra cluster addons list               # List installed add-ons\nankra cluster addons available          # List available (installable) add-ons\nankra cluster addons settings <addon>   # Show current settings for an add-on\nankra cluster addons update <addon>     # Update add-on configuration\nankra cluster addons uninstall <addon>  # Uninstall an add-on\n```\n\n## Stacks (Application Stacks)\n\n```bash\nankra cluster stacks list               # List stacks in the cluster\nankra cluster stacks create <name>      # Create a new stack\nankra cluster stacks delete <name>      # Delete a stack\nankra cluster stacks rename <old> <new> # Rename a stack\nankra cluster stacks clone <src> <dst>  # Clone a stack\nankra cluster stacks history <name>     # View stack change history\n```\n\n- Prefer `clone` over `create` when replicating an existing working stack\n- Check `history` before rolling back to understand what changed\n\n## Manifests & Operations\n\n```bash\nankra cluster manifests list            # List deployed manifests\nankra cluster operations list           # List in-progress or recent operations\nankra cluster operations cancel <id>    # Cancel a running operation\n```\n\n## Cluster Agent\n\n```bash\nankra cluster agent status              # Check agent health\nankra cluster agent token               # Retrieve agent bootstrap token\nankra cluster agent upgrade             # Upgrade the in-cluster agent\n```\n\n- Run `agent status` if cluster commands seem unresponsive\n- Rotate the agent token periodically for security\n\n## Helm\n\n```bash\nankra cluster helm releases             # List Helm releases in the cluster\nankra cluster helm uninstall <release>  # Uninstall a Helm release\nankra helm registries                   # List configured Helm registries\nankra helm credentials                  # List Helm registry credentials\n```\n\n## Hetzner Cloud Clusters\n\n```bash\nankra cluster hetzner create            # Create a Hetzner-backed cluster\nankra cluster hetzner scale             # Scale the cluster node count\nankra cluster hetzner upgrade           # Upgrade Kubernetes version\nankra cluster hetzner workers           # Manage worker node groups\nankra cluster hetzner node-group        # Node group management\n```\n\n## Charts\n\n```bash\nankra charts list                       # List available charts\nankra charts search <term>              # Search charts\nankra charts info <chart>               # Detailed chart information\n```\n\n## Credentials\n\n```bash\nankra credentials list                              # List stored credentials\nankra credentials get <name>                        # Retrieve a credential\nankra credentials validate <name>                   # Validate a credential\nankra credentials delete <name>                     # Delete a credential\nankra credentials hetzner --name <n> --token <t>   # Store Hetzner API token\nankra credentials ovh ...                           # Store OVH credentials\nankra credentials upcloud ...                       # Store UpCloud credentials\n```\n\n- Never commit credentials to source control; use `ANKRA_API_TOKEN` env var\n- Validate credentials immediately after creation: `ankra credentials validate <name>`\n\n## Tokens (API Tokens)\n\n```bash\nankra tokens list                       # List API tokens\nankra tokens create --name <name>       # Create a new API token\nankra tokens revoke <id>                # Revoke a token (keeps record)\nankra tokens delete <id>                # Permanently delete a token\n```\n\n## AI Troubleshooting\n\n```bash\nankra chat                              # Start AI-powered troubleshooting session\n```\n\nUse `ankra chat` when facing ambiguous errors — it provides contextual guidance.\n\n## Global Flags\n\n| Flag | Env Var | Description |\n|------|---------|-------------|\n| `--base-url <url>` | `ANKRA_BASE_URL` | Override API base URL |\n| `--token <token>` | `ANKRA_API_TOKEN` | API authentication token |\n| `--config <path>` | — | Path to config file (default `~/.ankra.yaml`) |\n\n## Scripting & Automation Tips\n\n- Export `ANKRA_API_TOKEN` in CI pipelines; never hard-code tokens\n- Use `--token` for one-off scripts where env var injection is not possible\n- Chain `ankra cluster select` before cluster-scoped commands in scripts\n- Pipe output through `jq` for structured processing where JSON output is available\n- Use `ankra cluster operations list` in CI to poll for async operation completion\n- Add `set -euo pipefail` to shell scripts using the Ankra CLI\n\n## Common Workflows\n\n### Provision and configure a new Hetzner cluster\n\n```bash\nankra login\nankra org switch my-org\nankra cluster hetzner create\nankra cluster select my-new-cluster\nankra cluster agent status\nankra cluster addons available\nankra cluster addons update some-addon\nankra cluster reconcile\n```\n\n### Deploy a stack from a chart\n\n```bash\nankra cluster select my-cluster\nankra charts search my-app\nankra charts info my-app\nankra cluster stacks create my-stack\nankra cluster manifests list\n```\n\n### Rotate credentials safely\n\n```bash\nankra tokens create --name ci-token-new\n# Update CI secret with new token\nankra tokens revoke <old-token-id>\nankra credentials validate my-cloud-cred\n```\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/snyk/cli/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n**Scanning for vulnerabilities?** Use agent mode: `snyk agent test --experimental` (experimental, subject to change). It runs Open Source, Code, and Secrets in one pass and returns compact, token-efficient output built for agents (`snyk agent --help`).\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Environment variables\n\nThe config reads only the variables allowlisted in `cliv2/pkg/core/main.go`:\n\n```go\nconfiguration.WithSupportedEnvVars(\"NODE_EXTRA_CA_CERTS\"),\nconfiguration.WithSupportedEnvVarPrefixes(\"snyk_\", \"internal_\", \"test_\"),\n```\n\nAnything else is invisible. `config.GetString(\"MY_VAR\")` returns `\"\"` however the shell is set. Names an external convention forces on us are added one at a time by full name.\n\nPick the prefix by who reads the value, not who sets it.\n\n| Prefix           | Read by                                          | Contract                                          |\n| ---------------- | ------------------------------------------------ | ------------------------------------------------- |\n| `SNYK_`          | Users, CI, IDEs                                  | Public and documented. Renaming one breaks users. |\n| `INTERNAL_`      | The Go CLI, through the config                   | None. Change it whenever.                         |\n| `SNYK_INTERNAL_` | The legacy TypeScript CLI, through `process.env` | None.                                             |\n| `TEST_`          | Tests                                            | None. Never read one from production code.        |\n\n- Read with `config.GetString`, never `os.Getenv`, so flags, config files, alternative keys and caching still apply.\n- A wrapper or an IDE setting the variable does not make it public. `INTERNAL_SNYK_CLIENT_MACHINE_ID` (`cliv2/pkg/core/instrumentation.go`) is set by another Snyk process and read by the CLI.\n- To put a public name on an internal key, add an alternative key: `config.AddAlternativeKeys(cliv2.ConfigKeyRequestConcurrency, []string{\"snyk_request_concurrency\"})`. Code keeps reading `internal_request_concurrency`, users set `SNYK_REQUEST_CONCURRENCY`.\n- The legacy CLI is a child process with no config engine, so values reach it only if you pass them: a constant in `cliv2/internal/constants/constants.go`, assigned in `fillEnvironmentFromConfig` (`cliv2/internal/cliv2/cliv2.go`). See `SNYK_INTERNAL_ORGID`.\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n### Tests must see what customers see\n\nWhen an acceptance test fails, find out why before changing anything. Users often hit the same thing: a feature that is off by default, a flag they would have to set, a code path the shipped binary never takes. Report it. Don't hide it by adding env vars or feature flags, stubbing the thing under test, loosening assertions, or skipping the test. If you think the setup is legitimate, ask first and leave a comment explaining it.\nCI builds carry a `-dev` or `-preview` version, and GAF turns preview features on for those. Do not set `INTERNAL_PREVIEW_FEATURES_ENABLED: 'true'` when testing customer-facing behaviour.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nBefore creating or editing a PR, read `.github/PULL_REQUEST_TEMPLATE.md` and fill its relevant sections, including **\"What's the product update that needs to be communicated to CLI users?\"**, manual testing instructions, and a risk assessment. This applies to dependency-only bumps too: describe the user-visible fix brought in by the dependency, link the upstream change and regression coverage, and state any remaining limitations. Check previous reviewer feedback before asking for review. Keep the description within 200 words (250 hard ceiling), and distinguish completed validation from pending or blocked checks.\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n### Handling `go.sum` and private modules\n\nAlways use Go tooling (`go get`, `go mod tidy`, `make tidy`) to update `go.mod` and `go.sum`. Never edit `go.sum` manually — its entries must be in lexicographic order, and manual edits easily break sorting.\n\nWhen private modules (e.g. `ambient-canary`) block `go mod tidy`, use `go mod tidy -e` to tolerate fetch errors while still maintaining correct formatting and sort order. If manual `go.sum` editing is truly unavoidable (last resort only), entries must be inserted in sorted position — never appended to the end of the file. Verify with `sort -c go.sum` after any manual edit.\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n","isInternal":false,"tokens":5282,"sizeBytes":22552},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/snyk/cli/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n**Scanning for vulnerabilities?** Use agent mode: `snyk agent test --experimental` (experimental, subject to change). It runs Open Source, Code, and Secrets in one pass and returns compact, token-efficient output built for agents (`snyk agent --help`).\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Environment variables\n\nThe config reads only the variables allowlisted in `cliv2/pkg/core/main.go`:\n\n```go\nconfiguration.WithSupportedEnvVars(\"NODE_EXTRA_CA_CERTS\"),\nconfiguration.WithSupportedEnvVarPrefixes(\"snyk_\", \"internal_\", \"test_\"),\n```\n\nAnything else is invisible. `config.GetString(\"MY_VAR\")` returns `\"\"` however the shell is set. Names an external convention forces on us are added one at a time by full name.\n\nPick the prefix by who reads the value, not who sets it.\n\n| Prefix           | Read by                                          | Contract                                          |\n| ---------------- | ------------------------------------------------ | ------------------------------------------------- |\n| `SNYK_`          | Users, CI, IDEs                                  | Public and documented. Renaming one breaks users. |\n| `INTERNAL_`      | The Go CLI, through the config                   | None. Change it whenever.                         |\n| `SNYK_INTERNAL_` | The legacy TypeScript CLI, through `process.env` | None.                                             |\n| `TEST_`          | Tests                                            | None. Never read one from production code.        |\n\n- Read with `config.GetString`, never `os.Getenv`, so flags, config files, alternative keys and caching still apply.\n- A wrapper or an IDE setting the variable does not make it public. `INTERNAL_SNYK_CLIENT_MACHINE_ID` (`cliv2/pkg/core/instrumentation.go`) is set by another Snyk process and read by the CLI.\n- To put a public name on an internal key, add an alternative key: `config.AddAlternativeKeys(cliv2.ConfigKeyRequestConcurrency, []string{\"snyk_request_concurrency\"})`. Code keeps reading `internal_request_concurrency`, users set `SNYK_REQUEST_CONCURRENCY`.\n- The legacy CLI is a child process with no config engine, so values reach it only if you pass them: a constant in `cliv2/internal/constants/constants.go`, assigned in `fillEnvironmentFromConfig` (`cliv2/internal/cliv2/cliv2.go`). See `SNYK_INTERNAL_ORGID`.\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n### Tests must see what customers see\n\nWhen an acceptance test fails, find out why before changing anything. Users often hit the same thing: a feature that is off by default, a flag they would have to set, a code path the shipped binary never takes. Report it. Don't hide it by adding env vars or feature flags, stubbing the thing under test, loosening assertions, or skipping the test. If you think the setup is legitimate, ask first and leave a comment explaining it.\nCI builds carry a `-dev` or `-preview` version, and GAF turns preview features on for those. Do not set `INTERNAL_PREVIEW_FEATURES_ENABLED: 'true'` when testing customer-facing behaviour.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nBefore creating or editing a PR, read `.github/PULL_REQUEST_TEMPLATE.md` and fill its relevant sections, including **\"What's the product update that needs to be communicated to CLI users?\"**, manual testing instructions, and a risk assessment. This applies to dependency-only bumps too: describe the user-visible fix brought in by the dependency, link the upstream change and regression coverage, and state any remaining limitations. Check previous reviewer feedback before asking for review. Keep the description within 200 words (250 hard ceiling), and distinguish completed validation from pending or blocked checks.\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n### Handling `go.sum` and private modules\n\nAlways use Go tooling (`go get`, `go mod tidy`, `make tidy`) to update `go.mod` and `go.sum`. Never edit `go.sum` manually — its entries must be in lexicographic order, and manual edits easily break sorting.\n\nWhen private modules (e.g. `ambient-canary`) block `go mod tidy`, use `go mod tidy -e` to tolerate fetch errors while still maintaining correct formatting and sort order. If manual `go.sum` editing is truly unavoidable (last resort only), entries must be inserted in sorted position — never appended to the end of the file. Verify with `sort -c go.sum` after any manual edit.\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n","isInternal":false,"tokens":5282,"sizeBytes":22552},{"name":"marketplace.json","path":".claude-plugin/marketplace.json","rawUrl":"https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/marketplace.json","title":"Claude Plugin Marketplace Catalog","category":"marketplace","format":"json","content":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/marketplace.json)","tokens":300,"sizeBytes":187},{"name":"plugin.json","path":".claude-plugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/plugin.json","title":"claude-plugin Plugin Manifest","category":"plugin-manifest","format":"json","content":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/plugin.json)","tokens":300,"sizeBytes":173},{"name":"mcp.json","path":".cursor/mcp.json","rawUrl":"https://raw.githubusercontent.com/snyk/cli/HEAD/.cursor/mcp.json","title":"Model Context Protocol (MCP) Configuration","category":"mcp-config","format":"json","content":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.cursor/mcp.json)","tokens":300,"sizeBytes":166},{"name":".cursorrules","path":".cursorrules (ankra-cli.mdc)","rawUrl":"https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/main/rules/ankra-cli.mdc","title":"Awesome Cursorrules: ankra cli","category":"cursor-rule","format":"markdown","content":"---\ndescription: \"Ankra CLI rules and best practices for managing Kubernetes clusters via the Ankra platform\"\nglobs: **/*.sh, **/*.yaml, **/*.yml, Makefile, **/Makefile, **/*.md\nalwaysApply: false\n---\n# Ankra CLI Best Practices\n\nAnkra is a Kubernetes cluster management platform. Its CLI (`ankra`) lets you manage organisations, clusters, add-ons, stacks, Helm releases, credentials, and tokens from the terminal.\n\n## Authentication & Configuration\n\n- Authenticate first: `ankra login`\n- Default config lives at `~/.ankra.yaml`; override with `--config <path>`\n- Override the API base URL with `--base-url` or `ANKRA_BASE_URL`\n- Supply an API token via `--token` or `ANKRA_API_TOKEN` (prefer env var in CI)\n- Logout cleanly: `ankra logout`\n\n```yaml\n# ~/.ankra.yaml example\nbase_url: https://api.ankra.io\ntoken: ${ANKRA_API_TOKEN}\n```\n\n## Organisation Management\n\n```bash\nankra org list                          # List accessible organisations\nankra org switch <org-name>             # Switch active organisation\nankra org create <name>                 # Create a new organisation\nankra org invite <email>                # Invite a user to the current org\nankra org members                       # List members of the current org\n```\n\n## Cluster Lifecycle\n\n```bash\nankra cluster list                      # List all clusters in the org\nankra cluster select <name>             # Set the active cluster context\nankra cluster info                      # Show details about the active cluster\nankra cluster provision                 # Provision a new cluster\nankra cluster deprovision               # Deprovision (destroy) the active cluster\nankra cluster reconcile                 # Reconcile cluster to desired state\nankra delete cluster <name>             # Delete a named cluster\n```\n\n- Always run `ankra cluster info` before destructive operations\n- Use `ankra cluster reconcile` after config changes before assuming state is correct\n\n## Add-ons\n\n```bash\nankra cluster addons list               # List installed add-ons\nankra cluster addons available          # List available (installable) add-ons\nankra cluster addons settings <addon>   # Show current settings for an add-on\nankra cluster addons update <addon>     # Update add-on configuration\nankra cluster addons uninstall <addon>  # Uninstall an add-on\n```\n\n## Stacks (Application Stacks)\n\n```bash\nankra cluster stacks list               # List stacks in the cluster\nankra cluster stacks create <name>      # Create a new stack\nankra cluster stacks delete <name>      # Delete a stack\nankra cluster stacks rename <old> <new> # Rename a stack\nankra cluster stacks clone <src> <dst>  # Clone a stack\nankra cluster stacks history <name>     # View stack change history\n```\n\n- Prefer `clone` over `create` when replicating an existing working stack\n- Check `history` before rolling back to understand what changed\n\n## Manifests & Operations\n\n```bash\nankra cluster manifests list            # List deployed manifests\nankra cluster operations list           # List in-progress or recent operations\nankra cluster operations cancel <id>    # Cancel a running operation\n```\n\n## Cluster Agent\n\n```bash\nankra cluster agent status              # Check agent health\nankra cluster agent token               # Retrieve agent bootstrap token\nankra cluster agent upgrade             # Upgrade the in-cluster agent\n```\n\n- Run `agent status` if cluster commands seem unresponsive\n- Rotate the agent token periodically for security\n\n## Helm\n\n```bash\nankra cluster helm releases             # List Helm releases in the cluster\nankra cluster helm uninstall <release>  # Uninstall a Helm release\nankra helm registries                   # List configured Helm registries\nankra helm credentials                  # List Helm registry credentials\n```\n\n## Hetzner Cloud Clusters\n\n```bash\nankra cluster hetzner create            # Create a Hetzner-backed cluster\nankra cluster hetzner scale             # Scale the cluster node count\nankra cluster hetzner upgrade           # Upgrade Kubernetes version\nankra cluster hetzner workers           # Manage worker node groups\nankra cluster hetzner node-group        # Node group management\n```\n\n## Charts\n\n```bash\nankra charts list                       # List available charts\nankra charts search <term>              # Search charts\nankra charts info <chart>               # Detailed chart information\n```\n\n## Credentials\n\n```bash\nankra credentials list                              # List stored credentials\nankra credentials get <name>                        # Retrieve a credential\nankra credentials validate <name>                   # Validate a credential\nankra credentials delete <name>                     # Delete a credential\nankra credentials hetzner --name <n> --token <t>   # Store Hetzner API token\nankra credentials ovh ...                           # Store OVH credentials\nankra credentials upcloud ...                       # Store UpCloud credentials\n```\n\n- Never commit credentials to source control; use `ANKRA_API_TOKEN` env var\n- Validate credentials immediately after creation: `ankra credentials validate <name>`\n\n## Tokens (API Tokens)\n\n```bash\nankra tokens list                       # List API tokens\nankra tokens create --name <name>       # Create a new API token\nankra tokens revoke <id>                # Revoke a token (keeps record)\nankra tokens delete <id>                # Permanently delete a token\n```\n\n## AI Troubleshooting\n\n```bash\nankra chat                              # Start AI-powered troubleshooting session\n```\n\nUse `ankra chat` when facing ambiguous errors — it provides contextual guidance.\n\n## Global Flags\n\n| Flag | Env Var | Description |\n|------|---------|-------------|\n| `--base-url <url>` | `ANKRA_BASE_URL` | Override API base URL |\n| `--token <token>` | `ANKRA_API_TOKEN` | API authentication token |\n| `--config <path>` | — | Path to config file (default `~/.ankra.yaml`) |\n\n## Scripting & Automation Tips\n\n- Export `ANKRA_API_TOKEN` in CI pipelines; never hard-code tokens\n- Use `--token` for one-off scripts where env var injection is not possible\n- Chain `ankra cluster select` before cluster-scoped commands in scripts\n- Pipe output through `jq` for structured processing where JSON output is available\n- Use `ankra cluster operations list` in CI to poll for async operation completion\n- Add `set -euo pipefail` to shell scripts using the Ankra CLI\n\n## Common Workflows\n\n### Provision and configure a new Hetzner cluster\n\n```bash\nankra login\nankra org switch my-org\nankra cluster hetzner create\nankra cluster select my-new-cluster\nankra cluster agent status\nankra cluster addons available\nankra cluster addons update some-addon\nankra cluster reconcile\n```\n\n### Deploy a stack from a chart\n\n```bash\nankra cluster select my-cluster\nankra charts search my-app\nankra charts info my-app\nankra cluster stacks create my-stack\nankra cluster manifests list\n```\n\n### Rotate credentials safely\n\n```bash\nankra tokens create --name ci-token-new\n# Update CI secret with new token\nankra tokens revoke <old-token-id>\nankra credentials validate my-cloud-cred\n```\n","tokens":1574,"sizeBytes":7084}],"systemPromptSnippet":"<yakaai_skills repo=\"snyk/cli\">\n<!-- File: AGENTS.md (Tokens: ~5282 | Category: root-instruction) -->\n# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n**Scanning for vulnerabilities?** Use agent mode: `snyk agent test --experimental` (experimental, subject to change). It runs Open Source, Code, and Secrets in one pass and returns compact, token-efficient output built for agents (`snyk agent --help`).\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Environment variables\n\nThe config reads only the variables allowlisted in `cliv2/pkg/core/main.go`:\n\n```go\nconfiguration.WithSupportedEnvVars(\"NODE_EXTRA_CA_CERTS\"),\nconfiguration.WithSupportedEnvVarPrefixes(\"snyk_\", \"internal_\", \"test_\"),\n```\n\nAnything else is invisible. `config.GetString(\"MY_VAR\")` returns `\"\"` however the shell is set. Names an external convention forces on us are added one at a time by full name.\n\nPick the prefix by who reads the value, not who sets it.\n\n| Prefix           | Read by                                          | Contract                                          |\n| ---------------- | ------------------------------------------------ | ------------------------------------------------- |\n| `SNYK_`          | Users, CI, IDEs                                  | Public and documented. Renaming one breaks users. |\n| `INTERNAL_`      | The Go CLI, through the config                   | None. Change it whenever.                         |\n| `SNYK_INTERNAL_` | The legacy TypeScript CLI, through `process.env` | None.                                             |\n| `TEST_`          | Tests                                            | None. Never read one from production code.        |\n\n- Read with `config.GetString`, never `os.Getenv`, so flags, config files, alternative keys and caching still apply.\n- A wrapper or an IDE setting the variable does not make it public. `INTERNAL_SNYK_CLIENT_MACHINE_ID` (`cliv2/pkg/core/instrumentation.go`) is set by another Snyk process and read by the CLI.\n- To put a public name on an internal key, add an alternative key: `config.AddAlternativeKeys(cliv2.ConfigKeyRequestConcurrency, []string{\"snyk_request_concurrency\"})`. Code keeps reading `internal_request_concurrency`, users set `SNYK_REQUEST_CONCURRENCY`.\n- The legacy CLI is a child process with no config engine, so values reach it only if you pass them: a constant in `cliv2/internal/constants/constants.go`, assigned in `fillEnvironmentFromConfig` (`cliv2/internal/cliv2/cliv2.go`). See `SNYK_INTERNAL_ORGID`.\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n### Tests must see what customers see\n\nWhen an acceptance test fails, find out why before changing anything. Users often hit the same thing: a feature that is off by default, a flag they would have to set, a code path the shipped binary never takes. Report it. Don't hide it by adding env vars or feature flags, stubbing the thing under test, loosening assertions, or skipping the test. If you think the setup is legitimate, ask first and leave a comment explaining it.\nCI builds carry a `-dev` or `-preview` version, and GAF turns preview features on for those. Do not set `INTERNAL_PREVIEW_FEATURES_ENABLED: 'true'` when testing customer-facing behaviour.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nBefore creating or editing a PR, read `.github/PULL_REQUEST_TEMPLATE.md` and fill its relevant sections, including **\"What's the product update that needs to be communicated to CLI users?\"**, manual testing instructions, and a risk assessment. This applies to dependency-only bumps too: describe the user-visible fix brought in by the dependency, link the upstream change and regression coverage, and state any remaining limitations. Check previous reviewer feedback before asking for review. Keep the description within 200 words (250 hard ceiling), and distinguish completed validation from pending or blocked checks.\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n### Handling `go.sum` and private modules\n\nAlways use Go tooling (`go get`, `go mod tidy`, `make tidy`) to update `go.mod` and `go.sum`. Never edit `go.sum` manually — its entries must be in lexicographic order, and manual edits easily break sorting.\n\nWhen private modules (e.g. `ambient-canary`) block `go mod tidy`, use `go mod tidy -e` to tolerate fetch errors while still maintaining correct formatting and sort order. If manual `go.sum` editing is truly unavoidable (last resort only), entries must be inserted in sorted position — never appended to the end of the file. Verify with `sort -c go.sum` after any manual edit.\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n\n<!-- File: CLAUDE.md (Tokens: ~5282 | Category: claude-rule) -->\n# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n**Scanning for vulnerabilities?** Use agent mode: `snyk agent test --experimental` (experimental, subject to change). It runs Open Source, Code, and Secrets in one pass and returns compact, token-efficient output built for agents (`snyk agent --help`).\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Environment variables\n\nThe config reads only the variables allowlisted in `cliv2/pkg/core/main.go`:\n\n```go\nconfiguration.WithSupportedEnvVars(\"NODE_EXTRA_CA_CERTS\"),\nconfiguration.WithSupportedEnvVarPrefixes(\"snyk_\", \"internal_\", \"test_\"),\n```\n\nAnything else is invisible. `config.GetString(\"MY_VAR\")` returns `\"\"` however the shell is set. Names an external convention forces on us are added one at a time by full name.\n\nPick the prefix by who reads the value, not who sets it.\n\n| Prefix           | Read by                                          | Contract                                          |\n| ---------------- | ------------------------------------------------ | ------------------------------------------------- |\n| `SNYK_`          | Users, CI, IDEs                                  | Public and documented. Renaming one breaks users. |\n| `INTERNAL_`      | The Go CLI, through the config                   | None. Change it whenever.                         |\n| `SNYK_INTERNAL_` | The legacy TypeScript CLI, through `process.env` | None.                                             |\n| `TEST_`          | Tests                                            | None. Never read one from production code.        |\n\n- Read with `config.GetString`, never `os.Getenv`, so flags, config files, alternative keys and caching still apply.\n- A wrapper or an IDE setting the variable does not make it public. `INTERNAL_SNYK_CLIENT_MACHINE_ID` (`cliv2/pkg/core/instrumentation.go`) is set by another Snyk process and read by the CLI.\n- To put a public name on an internal key, add an alternative key: `config.AddAlternativeKeys(cliv2.ConfigKeyRequestConcurrency, []string{\"snyk_request_concurrency\"})`. Code keeps reading `internal_request_concurrency`, users set `SNYK_REQUEST_CONCURRENCY`.\n- The legacy CLI is a child process with no config engine, so values reach it only if you pass them: a constant in `cliv2/internal/constants/constants.go`, assigned in `fillEnvironmentFromConfig` (`cliv2/internal/cliv2/cliv2.go`). See `SNYK_INTERNAL_ORGID`.\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n### Tests must see what customers see\n\nWhen an acceptance test fails, find out why before changing anything. Users often hit the same thing: a feature that is off by default, a flag they would have to set, a code path the shipped binary never takes. Report it. Don't hide it by adding env vars or feature flags, stubbing the thing under test, loosening assertions, or skipping the test. If you think the setup is legitimate, ask first and leave a comment explaining it.\nCI builds carry a `-dev` or `-preview` version, and GAF turns preview features on for those. Do not set `INTERNAL_PREVIEW_FEATURES_ENABLED: 'true'` when testing customer-facing behaviour.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nBefore creating or editing a PR, read `.github/PULL_REQUEST_TEMPLATE.md` and fill its relevant sections, including **\"What's the product update that needs to be communicated to CLI users?\"**, manual testing instructions, and a risk assessment. This applies to dependency-only bumps too: describe the user-visible fix brought in by the dependency, link the upstream change and regression coverage, and state any remaining limitations. Check previous reviewer feedback before asking for review. Keep the description within 200 words (250 hard ceiling), and distinguish completed validation from pending or blocked checks.\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n### Handling `go.sum` and private modules\n\nAlways use Go tooling (`go get`, `go mod tidy`, `make tidy`) to update `go.mod` and `go.sum`. Never edit `go.sum` manually — its entries must be in lexicographic order, and manual edits easily break sorting.\n\nWhen private modules (e.g. `ambient-canary`) block `go mod tidy`, use `go mod tidy -e` to tolerate fetch errors while still maintaining correct formatting and sort order. If manual `go.sum` editing is truly unavoidable (last resort only), entries must be inserted in sorted position — never appended to the end of the file. Verify with `sort -c go.sum` after any manual edit.\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n\n<!-- File: .claude-plugin/marketplace.json (Tokens: ~300 | Category: marketplace) -->\n# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/marketplace.json)\n\n<!-- File: .claude-plugin/plugin.json (Tokens: ~300 | Category: plugin-manifest) -->\n# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.claude-plugin/plugin.json)\n\n<!-- File: .cursor/mcp.json (Tokens: ~300 | Category: mcp-config) -->\n# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/snyk/cli/HEAD/.cursor/mcp.json)\n\n<!-- File: .cursorrules (ankra-cli.mdc) (Tokens: ~1574 | Category: cursor-rule) -->\n---\ndescription: \"Ankra CLI rules and best practices for managing Kubernetes clusters via the Ankra platform\"\nglobs: **/*.sh, **/*.yaml, **/*.yml, Makefile, **/Makefile, **/*.md\nalwaysApply: false\n---\n# Ankra CLI Best Practices\n\nAnkra is a Kubernetes cluster management platform. Its CLI (`ankra`) lets you manage organisations, clusters, add-ons, stacks, Helm releases, credentials, and tokens from the terminal.\n\n## Authentication & Configuration\n\n- Authenticate first: `ankra login`\n- Default config lives at `~/.ankra.yaml`; override with `--config <path>`\n- Override the API base URL with `--base-url` or `ANKRA_BASE_URL`\n- Supply an API token via `--token` or `ANKRA_API_TOKEN` (prefer env var in CI)\n- Logout cleanly: `ankra logout`\n\n```yaml\n# ~/.ankra.yaml example\nbase_url: https://api.ankra.io\ntoken: ${ANKRA_API_TOKEN}\n```\n\n## Organisation Management\n\n```bash\nankra org list                          # List accessible organisations\nankra org switch <org-name>             # Switch active organisation\nankra org create <name>                 # Create a new organisation\nankra org invite <email>                # Invite a user to the current org\nankra org members                       # List members of the current org\n```\n\n## Cluster Lifecycle\n\n```bash\nankra cluster list                      # List all clusters in the org\nankra cluster select <name>             # Set the active cluster context\nankra cluster info                      # Show details about the active cluster\nankra cluster provision                 # Provision a new cluster\nankra cluster deprovision               # Deprovision (destroy) the active cluster\nankra cluster reconcile                 # Reconcile cluster to desired state\nankra delete cluster <name>             # Delete a named cluster\n```\n\n- Always run `ankra cluster info` before destructive operations\n- Use `ankra cluster reconcile` after config changes before assuming state is correct\n\n## Add-ons\n\n```bash\nankra cluster addons list               # List installed add-ons\nankra cluster addons available          # List available (installable) add-ons\nankra cluster addons settings <addon>   # Show current settings for an add-on\nankra cluster addons update <addon>     # Update add-on configuration\nankra cluster addons uninstall <addon>  # Uninstall an add-on\n```\n\n## Stacks (Application Stacks)\n\n```bash\nankra cluster stacks list               # List stacks in the cluster\nankra cluster stacks create <name>      # Create a new stack\nankra cluster stacks delete <name>      # Delete a stack\nankra cluster stacks rename <old> <new> # Rename a stack\nankra cluster stacks clone <src> <dst>  # Clone a stack\nankra cluster stacks history <name>     # View stack change history\n```\n\n- Prefer `clone` over `create` when replicating an existing working stack\n- Check `history` before rolling back to understand what changed\n\n## Manifests & Operations\n\n```bash\nankra cluster manifests list            # List deployed manifests\nankra cluster operations list           # List in-progress or recent operations\nankra cluster operations cancel <id>    # Cancel a running operation\n```\n\n## Cluster Agent\n\n```bash\nankra cluster agent status              # Check agent health\nankra cluster agent token               # Retrieve agent bootstrap token\nankra cluster agent upgrade             # Upgrade the in-cluster agent\n```\n\n- Run `agent status` if cluster commands seem unresponsive\n- Rotate the agent token periodically for security\n\n## Helm\n\n```bash\nankra cluster helm releases             # List Helm releases in the cluster\nankra cluster helm uninstall <release>  # Uninstall a Helm release\nankra helm registries                   # List configured Helm registries\nankra helm credentials                  # List Helm registry credentials\n```\n\n## Hetzner Cloud Clusters\n\n```bash\nankra cluster hetzner create            # Create a Hetzner-backed cluster\nankra cluster hetzner scale             # Scale the cluster node count\nankra cluster hetzner upgrade           # Upgrade Kubernetes version\nankra cluster hetzner workers           # Manage worker node groups\nankra cluster hetzner node-group        # Node group management\n```\n\n## Charts\n\n```bash\nankra charts list                       # List available charts\nankra charts search <term>              # Search charts\nankra charts info <chart>               # Detailed chart information\n```\n\n## Credentials\n\n```bash\nankra credentials list                              # List stored credentials\nankra credentials get <name>                        # Retrieve a credential\nankra credentials validate <name>                   # Validate a credential\nankra credentials delete <name>                     # Delete a credential\nankra credentials hetzner --name <n> --token <t>   # Store Hetzner API token\nankra credentials ovh ...                           # Store OVH credentials\nankra credentials upcloud ...                       # Store UpCloud credentials\n```\n\n- Never commit credentials to source control; use `ANKRA_API_TOKEN` env var\n- Validate credentials immediately after creation: `ankra credentials validate <name>`\n\n## Tokens (API Tokens)\n\n```bash\nankra tokens list                       # List API tokens\nankra tokens create --name <name>       # Create a new API token\nankra tokens revoke <id>                # Revoke a token (keeps record)\nankra tokens delete <id>                # Permanently delete a token\n```\n\n## AI Troubleshooting\n\n```bash\nankra chat                              # Start AI-powered troubleshooting session\n```\n\nUse `ankra chat` when facing ambiguous errors — it provides contextual guidance.\n\n## Global Flags\n\n| Flag | Env Var | Description |\n|------|---------|-------------|\n| `--base-url <url>` | `ANKRA_BASE_URL` | Override API base URL |\n| `--token <token>` | `ANKRA_API_TOKEN` | API authentication token |\n| `--config <path>` | — | Path to config file (default `~/.ankra.yaml`) |\n\n## Scripting & Automation Tips\n\n- Export `ANKRA_API_TOKEN` in CI pipelines; never hard-code tokens\n- Use `--token` for one-off scripts where env var injection is not possible\n- Chain `ankra cluster select` before cluster-scoped commands in scripts\n- Pipe output through `jq` for structured processing where JSON output is available\n- Use `ankra cluster operations list` in CI to poll for async operation completion\n- Add `set -euo pipefail` to shell scripts using the Ankra CLI\n\n## Common Workflows\n\n### Provision and configure a new Hetzner cluster\n\n```bash\nankra login\nankra org switch my-org\nankra cluster hetzner create\nankra cluster select my-new-cluster\nankra cluster agent status\nankra cluster addons available\nankra cluster addons update some-addon\nankra cluster reconcile\n```\n\n### Deploy a stack from a chart\n\n```bash\nankra cluster select my-cluster\nankra charts search my-app\nankra charts info my-app\nankra cluster stacks create my-stack\nankra cluster manifests list\n```\n\n### Rotate credentials safely\n\n```bash\nankra tokens create --name ci-token-new\n# Update CI secret with new token\nankra tokens revoke <old-token-id>\nankra credentials validate my-cloud-cred\n```\n</yakaai_skills>"}