--- title: Hoppy CLI command tree type: docs date: 2026-05-09 status: active tags: [cli, reference, audit] --- # Hoppy CLI command tree Generated by walking `--help` against `target/release/hoppy` (iter-23). Use this as the source-of-truth map when auditing verb consistency or writing LLM prompts that need a command schema. > **Verb conventions** (from [[decision-log#cli-verb-conventions]]): > - **Top-level resources** use `create` / `delete` (e.g. `pull-zone create`, `dns zone delete`). > - **Items inside a parent collection** use `add` / `remove` (e.g. `dns record add`, `pull-zone hostname add`, `pull-zone referrer remove-allowed`). > - All collection nouns expose `list` and (where applicable) `get`. > - Mutation is always `update`. Never `edit` / `set` / `modify`. > - Destructive ops accept `-y`/`--yes`. Where `--dry-run` exists it's documented in `--help`. ## Top level | Command | Shape | Notes | |---|---|---| | `pull-zone` | container | CDN pull zones | | `storage-zone` | container | Storage zones (the metadata) | | `storage` | container | Files inside a storage zone | | `dns` | container | DNS zones + records | | `stream` | container | Video streaming | | `shield` | container | Security (WAF/DDoS/rate-limit) | | `script` | container | Edge scripting | | `container` | container | Magic Containers | | `db` | container | bunny.net Database (libSQL) | | `logs` | container | CDN access logs + origin error logs (retrieval only) | | `auth` | leaf | Validate API key, show billing | | `statistics` | leaf | Account-level CDN stats | | `video-library` | container | Library-level core API (DRM, transcribe stats) | | `purge` | leaf | Purge a single URL from cache | | `apikey` | container | Account API keys (iter-75) | | `billing` | container | Billing summary, payment requests, invoice PDFs (iter-75) | | `region` | container | Core CDN edge regions + pricing (iter-75) | | `country` | container | Countries recognised by bunny.net / valid ISO codes (iter-75) | | `search` | leaf | Global cross-resource search (iter-75) | | `user` | container | Account audit log (iter-75) | | `completions` | leaf | Generate shell completion scripts | ## `pull-zone` | Verb / sub | Description | |---|---| | `list`, `get`, `create`, `update`, `delete` | CRUD | | `count` | Total number of pull zones on the account (`GET /pullzone/count`) | | `check --name` | Name-availability preflight before `create` (`POST /pullzone/checkavailability`) | | `reset-security-key` | Rotate the token-auth `ZoneSecurityKey` (redacted unless `--reveal`) | | `purge` | Purge cache (full or by tag) | | `statistics` | Per-zone stats | | `hostname add` / `remove` / `load-free-cert` / `force-ssl` / `add-cert` / `remove-cert` / `update-key-type` / `request-external-cert` / `complete-external-cert` | Hostname + TLS lifecycle. `update-key-type --key-type rsa\|ec` switches the cert private-key algorithm (`POST /pullzone/{id}/updatePrivateKeyType`); `request-external-cert` → `complete-external-cert` is the two-step external-DNS cert flow (`POST /pullzone/{request,complete}ExternalDnsCertificate`) | | `edge-rule list` / `add` / `update` / `delete` / `enable` | Edge rules | | `referrer list` / `allow` / `remove-allowed` / `block` / `remove-blocked` | Referrer access control (anti-hotlinking) | | `ip list` / `block` / `unblock` | IP block list | ## `storage-zone` | Verb / sub | Description | |---|---| | `list`, `get`, `create`, `update`, `delete` | CRUD | | `delete --keep-linked-pull-zones` | Opt out of the destructive upstream default that also deletes linked pull zones | | `check --name` | Name-availability preflight before `create` (`POST /storagezone/checkavailability`) | | `regions` | List the storage regions available for `create` (`GET /storagezone/regions`) | | `reset-password` | Rotate the primary password / access key (redacted unless `--reveal`) | | `reset-read-only-password` | Rotate the read-only password (redacted unless `--reveal`) | | `statistics` | Per-zone stats | | `egress` | Per-protocol egress totals + charts (`GET /storagezone/{id}/statistics/egress`; `--date-from`/`--date-to`/`--hourly`) | ## `storage` | Verb / sub | Description | |---|---| | `upload` | Upload a file (streaming). `--checksum ` or bare `--checksum` sends a SHA-256 `Checksum` integrity header (bare = computed locally by streaming) | | `download` | Download a file (streaming) | | `ls` | List files (POSIX-style alias) | | `rm` | Delete a file, or a directory recursively when `--remote-path` has a trailing slash (POSIX-style alias) | > Inconsistency flagged for follow-up: `storage ls` / `rm` use POSIX shorthand; everywhere else the project uses `list` / `delete`. Decision: keep `ls`/`rm` because file ops are interactively used like a filesystem; document the exception here so it doesn't drift to other surfaces. ## `dns` ### `dns zone` `list`, `get`, `create`, `update`, `delete`, `check`, `statistics`, `export`, `import`, `dnssec`, `issue-cert`, `scan` - `check --domain` — domain-availability preflight (`POST /dnszone/checkavailability`). - `update --log-anonymization-type ` — last-octet anonymization mode when IP anonymization is enabled. ### `dns record` `list`, `add`, `update`, `delete` — `add` because record-in-zone is a child collection. - `list` is backed by the dedicated `GET /dnszone/{id}/records` endpoint (paginated: `--page`/`--per-page`/`--all`). - `add` / `update` expose the full record body: linked resources (`--pull-zone-id`, `--script-id`, `--accelerated`), smart routing (`--smart-routing-type none|latency|geolocation`, `--latency-zone`, `--geolocation-latitude`/`--geolocation-longitude`), health monitoring (`--monitor-type none|ping|http|monitor`), and `--auto-ssl-issuance`. `--type PullZone`/`Script` are now functional (previously accepted but non-configurable). ## `stream` ### `stream library` `list`, `get`, `create`, `update`, `delete`, `reset-api-key`, `reset-read-only-api-key`, `statistics`, `referrer`, `watermark`, `languages` - `reset-api-key` / `reset-read-only-api-key` rotate the library credentials; the new key is re-fetched and shown redacted unless `--reveal`. - `update` exposes the full `VideoLibraryUpdateModel` settings body (iter-73): encoding/resolution/codec settings (`--enabled-resolutions`, `--output-codecs`, `--keep-original-files`, `--jit-encoding-enabled`, `--allow-early-play`), player config (`--player-key-color`, `--captions-*`, `--font-family`, `--ui-language`, `--controls`, `--playback-speeds`, `--vast-tag-url`, `--custom-html`, `--show-heatmap`, `--remember-player-position`), `--webhook-url`, access control (`--enable-token-authentication`, `--enable-token-ip-verification`, `--block-none-referrer`), `--enable-drm`, and transcription defaults (`--enable-transcribing`, `--enable-transcribing-*-generation`, `--transcribing-caption-language`, `--enable-content-tagging`). Only the flags you pass are sent. - `referrer allow` / `remove-allowed` / `block` / `remove-blocked` — anti-hotlinking for the library (`POST /videolibrary/{id}/{add,remove}{Allowed,Blocked}Referrer`), mirroring the `pull-zone referrer` shape. - `watermark set --file ` / `delete` — upload (streamed, octet-stream) or clear the library watermark image (`PUT`/`DELETE /videolibrary/{id}/watermark`). `--has-watermark` on `update` only toggles the flag. - `languages` → `GET /videolibrary/languages` — player/transcription language list. ### `stream video` `list`, `get`, `upload`, `update`, `fetch`, `delete`, `caption`, `transcribe`, `heatmap`, `reencode`, `repackage`, `smart-generate`, `set-thumbnail`, `resolutions`, `storage`, `oembed`, `play-data`, `play-heatmap` - `caption add` / `delete` (child collection) - `resolutions list` / `cleanup` - `upload --resumable` (iter-77) switches from the single-shot `PUT` to a TUS 1.0 resumable upload (`POST /tusupload`, presigned signature auth, chunked `PATCH`). Session/offset state is persisted to disk (`--state-dir`, default a `hoppy-tus` folder in the OS temp dir) so re-running the same `--library-id`/`--file` resumes from the server offset after an interruption; retries with backoff on transient chunk failures. `--chunk-size ` tunes the `PATCH` window (default 32 MiB). The same per-upload params (`--jit-enabled`, `--enabled-resolutions`, `--enabled-output-codecs`, `--transcribe-*`, `--source-language`, `--generate-*`) apply on both transports; on the TUS path they ride in the `Upload-Metadata` header. The state file is removed on success. - `update --config-json ` supplies nested `chapters` / `moments` / `metaTags` arrays (iter-73) alongside `--title` / `--collection-id`. - `oembed` (`GET /OEmbed`), `play-data` (`GET …/play`), `play-heatmap` (`GET …/play/heatmap`) are the player-facing read endpoints; `play-heatmap` is distinct from `heatmap` (`GET …/heatmap`). ### `stream collection` `list`, `get`, `create`, `update`, `delete` ## `shield` ### `shield zone` `list`, `get`, `get-by-pullzone`, `create`, `update` - `update` exposes `--waf-disabled-rule` / `--waf-log-only-rule` (both repeatable; each replaces the current list). Discover valid rule IDs via `shield waf managed-rules --id `. ### `shield waf` `profiles`, `list-rules`, `get-rule`, `add-rule`, `update-rule`, `delete-rule`, `triggered-rules`, `review-triggered-rule`, `recommend-triggered-rule`, `plan-segmentation`, `engine-config`, `managed-rules`, `enums` - `managed-rules --id ` → `GET /shield/waf/rules/{shieldZoneId}` — lists the managed rulesets/groups/rule-IDs (the rule-ID discovery path). - `enums` → `GET /shield/waf/enums` — resolves the raw-integer WAF flags (`--action-type`, `--operator-type`, …) to their names. > Inconsistency flagged: WAF uses `add-rule`/`delete-rule` (compound verbs) instead of grouping them under a `rule` sub-noun like `dns record`. Trade-off: the API itself describes WAF rules as flat operations on the zone, not a separate sub-resource. Decision: keep flat shape; document here. ### `shield rate-limit` `list`, `get`, `create`, `update`, `delete` ### `shield access-list` `list`, `get`, `create`, `update`, `delete`, `update-config`, `enums` - `enums` → `GET /shield/shield-zone/{id}/access-lists/enums` (access-list enum types and their values). ### `shield bot-detection` `get`, `update` (singleton — no list/create/delete) ### `shield bot-categorization` `list`, `set-bot`, `set-category` - `list --id ` → `GET /bot-categorization` — bots available for explicit allow/block, grouped by category. - `set-bot --id --bot-id --action <0|1|2>` → per-bot override (`0 = None/clear`, `1 = Block`, `2 = Allow`). - `set-category --id --category --action <0|1|2>` → category-wide action (`255 = System`). ### `shield custom-page` `get`, `upload`, `delete` - Manage the custom HTML response pages (`--page-type block` / `challenge`). `get` prints the raw HTML; `upload --html-file ` PUTs it. ### `shield api-guardian` `get`, `enums`, `upload`, `update`, `update-endpoint` - `upload`/`update` target the `/api-guardian/spec` sub-resource (the bare `/api-guardian` POST/PATCH route was removed upstream). - `enums` → `GET /api-guardian/enums` (enumeration types and their values). ### `shield metrics` `overview`, `detailed`, `rate-limits`, `rate-limit`, `waf-rule`, `bot-detection`, `upload-scanning`, `overages`, `api-guardian` - `overages --id [--year --month]` → `GET /shield/metrics/overages/{id}` (monthly overage report). - `api-guardian --id [--endpoint-id ]` → aggregate API Guardian metrics (or per-endpoint when `--endpoint-id` is given). ### Other shield leaves `upload-scanning`, `event-logs`, `pullzone-mapping`, `ddos-enums` - `ddos-enums` → `GET /shield/ddos/enums` (resolves raw-integer DDoS flags to names). ## `script` `list`, `get`, `create`, `update`, `delete`, `code`, `publish`, `release`, `variable`, `secret`, `statistics`, `rotate-deployment-key` - `code get` / `update` (singleton — there is one draft per script) - `release list` / `get-active` - `variable list` / `add` / `update` / `delete` / `upsert` (child collection) - `secret list` / `add` / `update` / `delete` / `upsert` (child collection) ## `container` ### `container app` `list`, `get`, `create`, `update`, `summary`, `deploy`, `undeploy`, `restart`, `delete`, `overview`, `statistics`, `autoscaling-get`, `autoscaling-update`, `region-settings-get`, `region-settings-update` - `create` / `update` accept `--volume NAME:SIZE_GB` (repeatable) to declare persistent volumes — the only CLI path that *creates* a volume (iter-76). On `update` the flag sends the full `volumes` array (PATCH replace semantics). - `summary` prints the schema-less `GET /apps/{id}/summary` body as raw JSON (no typed model — live-verify before adding one; iter-76). ### `container template` `get`, `add`, `update`, `delete`, `env` — `add` because templates live inside an app. - `add` / `update` expose container runtime config (iter-76): `--image-digest sha256:…`, `--pull-policy Always|IfNotPresent`, entrypoint flags (`--command`, `--command-array` (repeatable, wins over `--command`), `--arguments`, `--arguments-array`, `--working-directory`), `--probes-json ` (a JSON file matching the `ContainerProbes` shape — startup/readiness/ liveness), and `--volume-mount NAME:MOUNT_PATH` (repeatable). ### `container endpoint` `list`, `add`, `update`, `delete` - `add` / `update` expose CDN + anycast options (iter-76): `--ssl`, `--pull-zone-id`, `--sticky` + `--sticky-header` (1-3) + `--sticky-cookie` (CDN only), and `--port-mapping CONTAINER[:EXPOSED[:PROTO,...]]` (repeatable, protocols `Tcp`/`Udp`/`Sctp`). Anycast endpoints accept several port mappings; CDN is capped at one. `--container-port`/`--exposed-port` remain as the single-mapping shorthand. ### `container volume` `list`, `update`, `detach`, `delete`, `delete-instance` - Volumes are *created* via `app create --volume` / `app update --volume` and *mounted* via `template add|update --volume-mount` (iter-76). These verbs manage existing volumes only. ### `container registry` `list`, `get`, `create`, `update`, `delete`, `images`, `image-config`, `image-tags`, `image-digest`, `config-suggestions`, `search-public` - `images --registry-id ` lists images in a registry (`POST /registries/images`; iter-76). - `image-config --registry-id --image-name --image-namespace --tag` prints the schema-less `POST /registries/image-config` body as raw JSON (iter-76). ### `container region` / `node` / `pod` / `limits` / `log-forwarding` `region show` / `node list|ips` / `pod recreate` / `limits` (singleton) / `log-forwarding list|get|create|update|delete` - `node ips` prints the schema-less `GET /nodes/plain` body as raw JSON (iter-76). ### Top-level shortcuts `container list` → `container app list` `container get` → `container app get` `container delete` → `container app delete` ## `db` `list`, `get`, `create`, `delete`, `fork`, `restore`, `versions`, `ping`, `statistics`, `usage`, `active-usage`, `live`, `v2` (subgroup), `group`, `token`, `config` - `group list|get|create|update|delete|stats|usage|live|generate-keys|invalidate-keys` - `token mint|invalidate|generate-v2|revoke-v2` - `config show|limits|optimal|optimal-single` - `v2 list|get|create|update|delete` ## `logs` Retrieval-only wrappers over two 0%-coverage services added in iter-70. Both authenticate with the standard `BUNNY_API_KEY`; base URLs are overridable via `BUNNY_LOGGING_URL` / `BUNNY_ORIGIN_ERRORS_URL` for tests/staging. | Verb / sub | Description | |---|---| | `pull-zone` | CDN access logs for a pull zone. v2 structured JSON by default (full filter + pagination set: `--status`, `--cache-status`, `--country`, `--edge-location`, `--remote-ip`, `--url-contains`, `--user-agent-contains`, `--referer-contains`, `--search`, `--request-id`, `--include-origin-shield`, `--limit`, `--offset`, `--order`, `--from`, `--to`). `--legacy --date ` streams the v1 raw pipe-delimited log (honours `--start`/`--end`/`--legacy-sort`/`--legacy-download`, and reuses `--status`/`--search` as single-value legacy filters). `--output ` streams to disk. | | `origin-errors` | CDN origin error logs for a pull zone on one date (`--date MM-DD-YYYY`, validated locally). | - These are `logs pull-zone` / `logs origin-errors` — not verbs on the `pull-zone` group — because they read a separate logging plane, not pull-zone config. - Retrieval only; log-forwarding *configuration* stays on `pull-zone update --log-forwarding-*` and `container log-forwarding`. ## `video-library` `drm-statistics`, `transcribing-statistics` (account/library-level core endpoints not covered by `stream library`) ## Account / admin (iter-75) An account/admin command group closing 8 of the previously-missing core misc ops (`GET /apikey`, `GET /billing/summary`, `GET /billing/payment-requests`, the two invoice PDF endpoints, `GET /region`, `GET /country`, `GET /search`, `GET /user/audit/{date}`). Only `GET /billing` was reachable before (as `auth check`). All authenticate with the standard `BUNNY_API_KEY`. ### `apikey` | Verb / sub | Description | |---|---| | `list` | List the account's API keys (`GET /apikey`, paginated `--page`/`--per-page`). Key values are redacted (``) in every format unless `--reveal` is passed. | ### `billing` | Verb / sub | Description | |---|---| | `summary` | Per-pull-zone billing summary for the current month (`GET /billing/summary`). | | `payment-requests` | List payment requests, open and settled (`GET /billing/payment-requests`). | | `invoice-pdf --record-id --output` | Download a billing-record invoice PDF (`GET /billing/summary/{billingRecordId}/pdf`); streamed chunk-by-chunk to the `--output` file. | | `payment-request-pdf --id --output` | Download a payment-request invoice PDF (`GET /billing/payment-request-invoice/{id}/pdf`); streamed to disk. | - The two PDF downloads never buffer the whole payload — they stream via the core client's `stream_to_writer` helper (project streaming rule). ### `region` | Verb / sub | Description | |---|---| | `list` | Core CDN edge regions with per-gigabyte pricing (`GET /region`). Distinct from the containers `container region` group and from `storage-zone regions`. | ### `country` | Verb / sub | Description | |---|---| | `list` | Countries recognised by bunny.net with ISO codes and tax info (`GET /country`). Documents the valid values for pull-zone country-blocking filters. | ### `search` `hoppy search [--from N] [--size N]` → `GET /search` — global cross-resource search over pull zones, storage zones, DNS zones, etc. `--from`/`--size` drive result-window pagination. ### `user` | Verb / sub | Description | |---|---| | `audit --date ` | Account audit log for a date (`GET /user/audit/{date}`). Filters: `--product`, `--resource-type`, `--resource-id`, `--actor-id` (all repeatable), `--order ascending\|descending`, `--continuation-token`, `--limit`. Paginated via the continuation token surfaced in a stderr hint / the JSON `ContinuationToken` field. | > `POST /user/closeaccount` and `GET /billing/affiliate` remain deliberately > unexposed (destructive / niche — gap-analysis decision, see > [[research/api-coverage-2026-07/pullzone-misc]]). ## Audit findings (deferred to follow-up iteration) - **`storage ls` / `rm`** — POSIX style; intentional but documented as an exception. - **`shield waf add-rule` / `delete-rule`** — flat compound verbs instead of `shield waf rule add`. Documented as intentional (matches API shape). - **`statistics` (top level)** — appears empty in `--help` listing. Confirm whether subcommands are gated or genuinely missing. - **`hoppy --help` doesn't print "next steps" hints** — hyalo's drill-down hints pattern (see iter-107 in `../hyalo`) would help LLM consumers. Filed as backlog item. ## Related - [[decision-log]] — verb conventions - [[help-text-style]] — LLM-friendly help text template - [[iterations/iteration-23-hyalo-best-practices]]