Use when writing or modifying npa CLI commands or SDK functions — registration, option naming, path contract, output format, error handling, exit codes, the decorators that are easy to forget, and config/credential access.
Installs into .claude/skills of the current project.
Are you the author of Npa Cli Conventions?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/nebius-npa-cli-conventions)
---
name: npa-cli-conventions
description: Use when writing or modifying npa CLI commands or SDK functions — registration, option naming, path contract, output format, error handling, exit codes, the decorators that are easy to forget, and config/credential access.
---
# npa CLI And SDK Conventions
Conventions for changing the `npa` package itself. For the full path of adding a
new tool, use `skills/workflows/add-workbench-tool/SKILL.md`.
The codebase is mixed: some modules predate the current pattern. Where this
skill and an existing module disagree, follow this skill and leave the old module
alone unless you are deliberately migrating it.
## Where Code Goes
Behavior belongs in `npa/src/npa/workbench/<tool_snake>/`. The CLI and SDK are
clients of it, not second implementations. Three integration patterns exist:
- **Workbench module first** — the SDK builds a Pydantic request and calls the
shared implementation, or HTTP when `service=True`. This is the pattern for
new work. See `npa/src/npa/sdk/workbench/dataset.py`.
- **CLI wrapper** — `make_cli_wrapper` in `npa/src/npa/_sdk.py` exposes an
existing CLI callback as an SDK function. Used by older orchestration-heavy
tools such as LeRobot. Do not choose it for new tools.
- **Direct module import** — for platform code with no service, such as
`npa/src/npa/sdk/provisioning.py`.
## Registration
Register workload command groups under `npa workbench` in
`npa/src/npa/cli/workbench/__init__.py`. Do not add top-level groups: the
platform utilities registered in `npa/src/npa/cli/main.py` predate the solution
namespace model and are marked as legacy in that file.
The operator desktop is explicitly registered under `npa tools desktop`, with
shared implementation in `npa/src/npa/tools/desktop` and CLI wiring in
`npa/src/npa/cli/tools`.
Use that namespace for its setup, display, HTTPS access, status, and open
commands. It is a development environment rather than a Workbench workload;
its Python surface calls the platform module directly and has no container or
workflow toolRef requirement.
## Options
The cross-tool handoff flags are `--input-path` (reads an `s3://` URI) and
`--output-path` (writes one). Validate them with the shared contract in
`npa/src/npa/cli/path_contract.py`. Public handoff paths must not require
VM-local paths, `file://` URIs, or plain HTTP URLs. A few legacy tools use
`--input-uri` / `--output-uri`; keep them working with aliases rather than
spreading the older spelling.
Conventional flags: `-p/--project` for the config alias, `-n/--name` for the
workbench instance, `--endpoint` to override a saved endpoint, `--service` to
call the deployed service instead of running in-process, and `--token-env` for
the env var holding a bearer token. Not every tool has all of them; check the
sibling module rather than assuming.
## Output
There is no shared output-format module. Each CLI package defines its own
`OutputFormat` enum and an `emit()` helper — copy the one next to you rather
than inventing a third shape. `npa/src/npa/cli/workbench/lancedb/helpers.py` is
a compact reference.
JSON flag spellings differ by area: `--output-format json` and `--json` on
workflow and platform commands, `--output json` on several workbench tools. For
anything an agent or script will parse, prefer `--output-format` plus
`@json_stdout_contract` from `npa/src/npa/lifecycle_intent.py`, which guarantees
exactly one JSON document on stdout.
## Errors And Exit Codes
Raise a domain error from the workbench module, map it to `HTTPException` in the
service, and let the CLI turn it into a message. `npa/src/npa/errors.py` holds
the shared base (`NpaError`); most tools define their own subclasses locally.
`npa/src/npa/cli/_error_formatting.py` renders serverless errors for humans or
as JSON.
The top-level handler is `app_entry()` in `npa/src/npa/cli/main.py`:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Serverless/client errors, and most command-local `typer.Exit(1)` |
| 2 | Unexpected exception |
| 130 | SIGINT |
On exit code 2 the CLI prints a hint instead of a traceback; `NPA_DEBUG=1`
restores the traceback. Never swallow an exception silently — a bare
`except Exception: pass` is rejected by
`npa/tests/guardrails/test_hygiene_guards.py`.
## Three Decorators That Are Easy To Forget
- `@resolve_typer_defaults` (`npa/src/npa/cli/_typer_defaults.py`) — required
when one Typer command calls another as a plain Python function. Without it an
unresolved `OptionInfo` object leaks into downstream code such as Terraform
variables or shell commands. Enforced by
`npa/tests/guardrails/test_typer_command_calls.py`.
- `@json_stdout_contract` (`npa/src/npa/lifecycle_intent.py`) — on any command
exposing a JSON flag.
- `@intent_boundary` (`npa/src/npa/lifecycle_intent.py`) — on entrypoints that
provision or destroy, so the lifecycle intent is inherited by child processes.
## Config And Credentials
Never hardcode project, tenant, registry, or bucket identifiers. Read them
through `npa/src/npa/clients/config.py` and
`npa/src/npa/clients/credentials.py`:
```python
from npa.clients.config import resolve_config, resolve_environment
from npa.clients.credentials import load_credentials
cfg = resolve_config(project=project, name=name)
env = resolve_environment(project=project)
creds = load_credentials()
```
Precedence is CLI flags, then environment variables, then
`~/.npa/credentials.yaml`, then `~/.npa/config.yaml`. Other accessors worth
knowing: `resolve_ssh_config`, `resolve_project_storage`,
`resolve_container_registry`, and the deep-merging `write_config` /
`write_credentials_file`.
Committed examples use placeholders such as `<your-project-id>` and
`<your-bucket>`. Concrete values are caught by gitleaks and the confidentiality
scan — see `skills/atomic/protect-nebius-infra-details/SKILL.md`.
## Subprocesses
To re-invoke the CLI internally, use `internal_cli_argv()` from
`npa/src/npa/cli/invocation.py`. It runs `python -m npa` on the active
interpreter rather than trusting an `npa` binary on `PATH`.
## Before You Push
Changing CLI options invalidates generated docs and can break catalog argv.
Regenerate with `bash scripts/build_docs.sh` and re-check the catalog contract
in `skills/atomic/toolref-argv-contract/SKILL.md`. Full gate order is in
`skills/atomic/pre-pr-validation/SKILL.md`.