Overview
The froglet command-line tool. Every dashboard operation, plus file upload/download, PTY tickets, and scripted scope minting.
The CLI talks to the Froglet API at /v1/... over HTTPS using a Bearer key. Source lives at clients/cli/; distributed on npm as @kortix/froglet-cli.
Install
npm install -g @kortix/froglet-cli pnpm add -g @kortix/froglet-cli # or from source cargo install --git https://github.com/kortix-ai/froglet froglet-cli
Supported: macOS arm64/x64, Linux arm64/x64 (glibc), Windows x64. See Installation for the auth flow.
Global flags
| Flag | Effect |
|---|---|
--json | Print the raw API payload. Designed for piping. |
--url <url> | Override the API base URL for one invocation. |
Every command supports --json and exits non-zero on failure. The JSON output is the unwrapped data field — no data/error container, just the payload.
Command groups
| Group | Covers |
|---|---|
auth | Log in, log out, show identity. |
me | Principal currently authenticating (workspace, role, scopes). |
sb | Sandbox lifecycle, exec, files, PTY, snapshots, backups, restore. |
templates | Template catalog + publish/unpublish/delete. |
builds | Build templates from Docker images. |
ports | Public HTTPS routes per sandbox. |
ssh-access | SSH bastion tokens per sandbox. |
vol | Persistent volumes, attach/detach/search. |
keys | Workspace API keys (issue, rotate, scope). |
members | Workspace member management. |
quotas | Usage + limits + raise-quota requests. |
admin | Platform-admin ops. Workspace keys get 403 platform_admin_required. |
Per-sandbox commands accept either the slug (demo) or the full UUID. Per-port / per-key / per-ssh-access commands need the full UUID — pull it from --json.
auth
froglet auth login [--token <fl_…>] [--url <url>] froglet auth logout froglet auth whoami
me
$ froglet me via api_key workspace acme (acme) role owner scopes workspace:read, members:read, sandboxes:read, …
For API-key auth, the CLI surfaces the issuer's role and the key's effective scopes (intersection of the key's stored patterns and the issuer's current scopes). Demoting the issuer auto-narrows every outstanding key on the next request.
quotas
froglet quotas # bar chart of all workspace limits froglet quotas check sandboxes --delta 1
check is a non-mutating "would this fit?" preflight. Exit code is 1 on rejection. The resource arg accepts friendly shortnames:
| Shortname | Canonical |
|---|---|
sandboxes | sandboxes_concurrent |
vcpu | vcpu_concurrent |
memory | memory_mb_concurrent |
disk | disk_gb_total |
volumes | volumes_total |
snapshots | snapshots_total |
keys | api_keys_total |
members | members_total |
templates | templates_per_workspace |
ports
froglet ports ls <sandbox> froglet ports expose <sandbox> --internal <port> [--label x] [--public|--private] [--ttl <sec>] froglet ports revoke <sandbox> <port-full-uuid> froglet ports share <sandbox> <port-full-uuid> [--ttl <sec>]
Routes follow the pattern https://<label>--<sandbox-slug>.froglet.sh. Reserved labels (api, admin) are rejected.
$ froglet ports expose demo --internal 8080 --label web exposed :8080 → http://web--demo.froglet.sh
--ttl 600 auto-revokes after 10 minutes. --private requires an access token. Mint a one-time share with ports share:
$ pid=$(froglet ports ls demo --json | jq -r '.[0].id') $ froglet ports share demo "$pid" --ttl 300 token (expires …): eyJhbGciOiJI…
ssh-access
froglet ssh-access ls <sandbox> froglet ssh-access new <sandbox> [--name <n>] [--ttl <hours>] froglet ssh-access rm <access-full-uuid>
Access goes through the bastion at ssh.froglet.sh:2222.
$ froglet ssh-access new demo --name laptop --ttl 4
created access c35d7262-…
ssh -p 2222 eyJhbGciOiJI…@ssh.froglet.sh
expires 2026-05-17T18:00:00.000ZPaste the ssh -p 2222 … line into your terminal.
keys
froglet keys ls froglet keys scopes # every valid scope, grouped froglet keys new --name <n> --scopes a,b,c # secret is shown ONCE froglet keys rm <key-full-uuid>
A key's effective scopes are the intersection of what you request at creation and what the issuer currently has — you can't mint a key with more authority than your own.
The keys ls table is wide because the scopes column is huge. Prefer --json | jq in scripts.
members
froglet members ls froglet members roles # built-in role presets froglet members role <user-id> <viewer|member|admin|owner> froglet members scopes <user-id> [--grant a,b] [--deny c,d] froglet members remove <user-id>
Assigning owner requires the workspace:transfer_ownership scope — only the owner preset carries it. The role gate is scope-based, so it works correctly under API-key auth.
admin
froglet admin hosts ls / drain <id> / undrain <id> froglet admin orders ls / new froglet admin cells ls
Platform-admin operations. A workspace key gets 403 platform_admin_required (by design). For break-glass DB ops, use apps/admin-cli (pnpm admin …) — that runs directly against the database.
Scripting
# find a slug, get the full UUID froglet sb ls --json | jq -r '.[] | select(.state=="running") | .id' # wait for state=running until [ "$(froglet sb get demo --json | jq -r .sandbox.state)" = "running" ]; do sleep 0.5 done # stream a build's log SSE url=$(froglet builds log-url <build-id>) curl -N -H "Authorization: Bearer $FROGLET_API_KEY" "$url"
Exit codes
| Code | Meaning |
|---|---|
0 | Success. |
1 | API returned an error (4xx/5xx). --json prints the body. |
| Other | sb exec propagates the guest command's exit code. |
Gotchas
- Quote the whole command to
sb exec. clap eats-aas a flag if passed separately. Usesb exec demo 'uname -a'. sb snap≠sb backup.snapis fast but not uploaded — lost if the host dies.backupwrites to R2 and is restorable across hosts.ports revoke/share,ssh-access rm,keys rm,vol rmneed the full UUID, not the 8-char prefix. Get it from--json:Sandboxes and volumes don't have this issue — slug resolution is built in.pid=$(froglet ports ls demo --json | jq -r '.[0].id') froglet ports revoke demo "$pid"
execandls2response bodies are snake_case (exit_code,modified_ms) while the rest of the API is camelCase. The CLI hides the difference; just be aware if you script against/v1/...directly.- The default API URL is
https://api.froglet.sh— nothttps://froglet.sh. Old configs that hard-coded the dashboard host will 404; re-runauth loginor setFROGLET_API_URL.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 unauthorized | Key missing / wrong env / config not saved. | froglet auth login again, or export FROGLET_API_KEY=…. |
403 forbidden_scope | Token doesn't carry the scope this route needs. | froglet keys scopes to see them; mint a fresh key with keys new --scopes …. |
403 platform_admin_required | You hit an admin/… route with a workspace key. | Use apps/admin-cli for platform ops. |
404 not_found on sb get <slug> | Stale slug or wrong workspace. | sb ls to see what's actually there. |
400 invalid_enum_value on quotas check | Resource name not recognised. | Use a shortname (sandboxes, vcpu, …) or the full canonical enum. |
500 reserved label | Tried --label api / --label admin on ports expose. | Pick a different label. |
503 not_configured on vol search | API process is missing FROGLET_VOLUMED_URL / FROGLET_VOLUMED_AUTH_TOKEN. | Operator-side fix. |
429 quota_exceeded | You're at the limit for the resource. | froglet quotas to see what's red; quotas request to raise it. |
decode response errors | CLI is older than the API and a field renamed. | npm i -g @kortix/froglet-cli@latest. |