Skip to main content
A sandbox is a container with GPUs that runs in Hedra’s cloud and is paid per GPU-hour from your API wallet. hedra-cli sandbox creates and stops sandboxes and opens shells, commands, output streams, file copies and port forwards in them. hedra-cli sandbox gpus lists the models your workspace can run, each with its memory, its price per GPU-hour after your workspace’s discount, and whether one is free right now. The examples below set GPU to one of them, such as b300:1.

Before you start

  • Install hedra-cli, or update an existing install, with npm install --global @hedra/cli@latest, or see SDKs and developer tools.
  • A funded API wallet. A sandbox’s GPU time is paid from the API wallet; see Billing.

Sign in with a key that holds the sandbox scopes

The sandbox commands need these key scopes: So exec, shell and cp need sandboxes:read and sandboxes:exec, port-forward needs sandboxes:read and sandboxes:write, and shell --gpu, which creates, opens and stops a sandbox, needs all three. secrets set, secrets rm and rotate-signing-key also need you to be a workspace owner or admin, or the member who pays for the workspace; a service key cannot run them. Sign in with:
The key auth login stores holds every scope except all and keys:manage, so it holds the three sandbox scopes whatever your role in the workspace. Running auth login again, or hedra-cli workspaces select, renews the key you already hold, or mints a new one, with that same set of scopes. A key you mint yourself holds the sandbox scopes when it lists them or holds all. On the console’s API keys page, any workspace member can mint a personal key that lists them. hedra-cli keys create needs a key that holds keys:manage, which the auth login key does not, and can grant only scopes that key holds. Only a workspace owner or admin, or the member who pays for the workspace, can mint a key that holds all, or a service key. A key minted before key scopes existed holds all. A key in HEDRA_API_KEY takes precedence over the one auth login stored, so CI and agents can use a key of their own:
hedra-cli auth status shows which credential the CLI is using. When a key lacks a scope, the command’s error names the missing scope.

Two kinds of sandbox command

The sandbox group has two kinds of command, which take the sandbox in different ways. API commands map one-to-one onto the v3 sandbox API and take the sandbox as --sandbox, either its name or its sandbox_ id:
Stream commands hold a connection open to the sandbox and take it as the first argument:
A name is 1 to 32 lowercase letters, digits and hyphens, starting and ending with a letter or digit, and not gpus, quota, secrets or signing-key. With --ports, it contains no --. It is unique among your creating, active and stopping sandboxes, so a name reaches your live sandbox of that name, or else your newest one. A stream command resolves the name once and keeps the id, so a reconnect never reaches a later sandbox that reuses the name. Stream commands connect over HTTP/2 to a separate Hedra host, which the API names for each session, so a firewall that allows only api.hedra.com blocks them. They use the same proxy and certificate settings as the API commands: HEDRA_CLI_PROXY, HTTPS_PROXY or ALL_PROXY, with NO_PROXY, and HEDRA_CLI_CA_BUNDLE, HEDRA_CLI_EXTRA_CA_CERTS or SSL_CERT_FILE. Run hedra-cli sandbox <command> --help for every flag. --dry-run checks a command’s arguments locally and sends nothing, so a value only Hedra checks, such as a GPU count over 99, passes it. API commands follow hedra-cli’s usual output: a table on a terminal and JSON when stdout is piped, with --format, --query and --quiet as for every other command. A stream command’s stdout is the sandbox’s own output, so it writes its own messages and errors to stderr. A usage error (exit code 3) is the exception: hedra-cli’s usual argument checker reports it, in its own format, which is JSON on stdout when stdout is piped.

The quickest start: a shell on a new sandbox

If your workspace has never created a sandbox, gpus answers that sandboxes are not available, because the first create or shell --gpu is what turns them on. Skip gpus and set GPU to a model from the table at the top of this page.
shell --gpu creates a sandbox named shell- and six random characters, waits until it is ready, opens a shell in it, and stops it when you exit the shell. Billing ends at the stop request. While it waits, it prints its progress on stderr, and it warns first when no GPU of that model is free (the sandbox may then wait up to 15 minutes for one) or when your workspace’s GPU quota is too small.
  • --max-duration stops the sandbox this long after it is created, from 2 minutes to 24 hours, such as 90m or 8h; the default is 24h.
  • The sandbox also stops on its own about 15 minutes after its last activity, so a sandbox whose CLI crashed or whose laptop went to sleep does not run for its full --max-duration. An open shell counts as activity from its start until it exits. A process left running in the background, such as a tmux server or a nohup or & job, does not.
  • It needs a terminal on stdin and stdout, in the foreground, and refuses before it creates anything when it has none. On Windows it needs a console: run it in Windows Terminal or a console window, since mintty and Git Bash are not consoles.
  • If the connection drops, the sandbox keeps running. hedra-cli prints how to reopen the shell with hedra-cli sandbox shell <name> and how to stop the sandbox. A reopened shell’s exit does not stop the sandbox; its idle limit or --max-duration does, or stop.

Create a sandbox

  • --gpu is <model>:<count>, with a count from 1 to 99 GPUs per instance.
  • --command replaces the image’s entrypoint. Give it a JSON array of the command and its arguments, as above, or pass it once per argument; use the array when an argument starts with -. Without it, the image’s ENTRYPOINT and CMD run. The sandbox ends when its main process exits, unless --restart on-failure or --restart always restarts it. The main process’s stdin stays open, so a shell or an interpreter run as the command waits for input.
  • --env takes a JSON object. Answers return only the keys.
  • --ttl-seconds is required: it stops the sandbox that many seconds after it is created, from 120 to 86,400 (24 hours). The sandbox stops sooner if you stop it, its command exits, or the wallet cannot pay. Longer reservations require a sales contract with Hedra; contact sales@hedra.com.
  • --ports '{"port": 8888}' serves that port at the sandbox’s url; see Reach a sandbox’s URL.
For a sandbox you work in interactively, with no main process, pass --idle true. It stays up until you stop it, its --ttl-seconds ends, or the wallet cannot pay. Add --idle-ttl-seconds to stop it sooner, once it has had no activity for that long. Activity is an exec, shell or cp session, from its start until its process exits, and a request to its url. A port-forward, an attach, and a process left running after its session ends are not activity.
The command returns once the sandbox is created. Its instances may still be starting: check with get.

List, inspect and stop

get shows each instance’s status and, for one that failed, its failure_reason, and the sandbox’s end_reason once it has stopped. stop answers with the sandbox in stopping and returns; Hedra records it ended once every instance has stopped, usually within a minute or two. Billing ends at the stop request. Its GPUs count against your workspace’s quota until its instances have stopped, so at full quota a create sent straight after a stop can be refused for quota for a few seconds; retry it.

Pass secrets to a sandbox

Store a value such as an access token once as a workspace secret, then name it when you create a sandbox. Only a workspace owner or admin, or the member who pays for the workspace, can set or delete a secret; a service key cannot. Pipe the request body on stdin with --json -, so the value stays out of your shell history and the process list. This form needs jq, and reads the value from HF_TOKEN, which must be exported:
--name HF_TOKEN --value "$HF_TOKEN" works too, but the value then appears in the process list while the command runs.
  • A name is uppercase letters, digits and underscores, does not start with a digit, has at most 63 characters, and never starts SANDBOX_, which the sandbox reserves. It is also the name of the environment variable the sandbox receives.
  • A value is 1 byte to 16 KiB of UTF-8, and is never returned.
  • Setting a name your workspace already holds replaces its value.
Name up to 32 secrets with --secrets on create. Every instance receives each one as an environment variable of the same name:
The exec line checks that the variable is set without printing it. shell --gpu has no secrets flag: create the sandbox with --secrets, then open it with shell <sandbox>.
secrets ls lists each secret’s name and when it was created, never its value.

Run commands

exec runs the command after -- without a shell, relays your stdin to it and its stdout and stderr back, and exits with the command’s own exit code. To use shell syntax, run a shell: -- sh -c 'cd /work && make'. A command that cannot start, such as one the image does not have, prints [hedra] the command could not start: and the reason, and exits 2, which a script cannot tell from the command itself exiting 2.
  • -t runs the command on a terminal, with yours in raw mode, for programs such as htop or vim. It needs a terminal on stdin and stdout, and to run in the foreground, not as a background job.
  • -n sends no input: the command sees the end of its input at once.
  • --env KEY=VALUE sets a variable for the command; repeat it for more.
  • --term sets the remote TERM under -t; by default it is yours, or xterm-256color when the image may not know yours.
shell <sandbox> opens an interactive shell in a running sandbox: the one the image’s SHELL variable names, else bash, else sh. Like -t, it needs a terminal on stdin and stdout, which on Windows means Windows Terminal or a console window, not mintty or Git Bash. Its exit does not stop the sandbox.

Read the output

logs prints the main process’s recent output and exits. While the sandbox runs, the output comes live from the sandbox. Once it has stopped, logs reads the stored copy, so it is also the command for reading a stopped sandbox’s output. The stored copy keeps only the newest part of a long output, and logs says when older output was dropped. While the sandbox runs, the live read can end early when the main process has written a large burst of output, such as tens of MiB at once, even over an ordinary connection. logs then reconnects, a moment apart, starting after the last output it printed, until it has caught up with the recent output, and exits 0. A live read holds only the recent output, so output written while logs was behind can be gone by the time it reconnects, and nothing marks the gap: after any reconnect, logs says on stderr that output may be missing, unless no reconnect printed anything and it then prints the rest from the stored copy, as described next. Only when it cannot reconnect, when the read keeps ending with nothing new printed, or after 20 reconnects or a minute of them, as when the sandbox keeps writing faster than your connection carries, does logs say so on stderr and print the rest from the stored copy, starting after the last output it printed. The output can then be incomplete, and logs says so: the stored copy can lack the newest lines, even minutes after they were written, and when it no longer holds the line after the last one printed live, the output in between is missing. If the stored copy cannot be read, logs exits with the code for that failure. The main process’s output is not a terminal, so many programs buffer it and it shows up late. For Python, run python -u or set PYTHONUNBUFFERED=1, as the create example above does. logs -f, and attach, which is the same, keep printing new output until you press Ctrl-C. They wait up to 15 minutes for a sandbox that is still starting, reconnect after a dropped connection, and when the sandbox stops, print the rest from the stored copy and exit. logs and attach read the first instance (replica 0). saved-logs is an API command and answers JSON, not the output as text: an object with the output in content and truncated set when older output was cut. Use it to read another replica’s stored output with --replica, or output since a time with --since:

Copy files

One side of cp is <sandbox>:<path> and the other a local path. It copies a file or a whole directory either way. An argument whose part before the first : has the form of a sandbox name or a sandbox_ id is a sandbox path, whether or not you have a sandbox of that name, so write a local file named notes:v2 as ./notes:v2.
  • When the destination is an existing directory, the source lands inside it under its own name. Otherwise the source takes the destination’s name, and its missing parent directories are created. A destination ending in / must already be a directory.
  • A source ending in /. copies the directory’s contents rather than the directory.
  • A relative sandbox path is relative to the image’s working directory.
  • A symlinked source copies what it links to.
  • The image needs sh and tar. With GNU tar, hedra-cli also checks that a copy out arrived whole.
  • When your file system treats two names as the same because they differ only in case, as macOS and Windows do by default, or in Unicode normalization, as macOS does, a copy out leaves out with a warning the second entry, and then exits 255.
A complete copy exits 0. A copy in whose tar in the sandbox fails exits with that tar’s code; an image without sh or tar exits 255. A copy out whose tar fails exits 255, and its error line includes tar in the sandbox exited <code>. A copy either way that left out or refused an entry or lost output also exits 255 and says what is missing. A copy out that fails after writing something does not remove what it wrote. Its error line says where it is, in a sentence or clause that may start with a capital: part of the copy was written into <path>, which already existed when the path it wrote to was already a directory, or else a partial copy was left at <path>. That path is the destination itself, or <destination>/<source name> when the destination is an existing directory; for a /. source it is the destination. Delete it, or copy again. The exception, when the image has GNU tar, is a copy whose every file arrived but whose tar exited 1 because a file changed while it read it: it still exits 255, its line says tar in the sandbox exited 1 and every entry it sent arrived at <path>, and it names no partial copy. A copy out that fails before writing anything removes the parent directories it created. Ctrl-C stops a copy and exits 130. A copy out writes nothing more, waits up to 5 seconds for its last write to finish and for the modes of your directories it opened up to be put back (on macOS and Linux, a copy that writes into directories an earlier copy left without your read, write or search permission grants that permission while it writes), and then says where its partial copy is, as above, or removes the parent directories it created when it wrote nothing. A second Ctrl-C ends that wait at once. When the copy has not stopped by then, it says the modes of directories the copy opened up may not have been put back, after the partial-copy clause when there is one. On Windows:
  • A one-letter name before : is a drive, so c:\data is a local path; name a sandbox with a one-letter name by its sandbox_ id.
  • In a copy out, a symlink is left out with a warning, and the copy then exits 255.
  • A copy in writes files with mode 0644 and directories 0755, so run a script you copied in with sh script, or chmod +x it first.
  • In a copy out, an entry whose name Windows cannot hold, such as one containing \ or :, naming a device such as CON, or ending in a dot or a space, is left out with a warning, and the copy then exits 255.

Forward a port

port-forward listens on your machine’s 127.0.0.1 and [::1], or on the one of them your machine has, and forwards each connection to the port in the sandbox until you press Ctrl-C. Give one port for both sides, or <local>:<remote>; a local port of 0 picks a free one, which it prints. Up to 90 connections are open at once; one more waits for one to close. When your machine has only one of the two addresses, port-forward warns not listening on IPv6 ([::1]): <error>, or the same for IPv4 (127.0.0.1), and listens on the other. It refuses, with exit code 19, a local port it cannot listen on, with cannot listen on port <port> over IPv4 (127.0.0.1): <error>, or the same for IPv6 ([::1]). That includes a port another program listens on at 0.0.0.0 or [::], since that program’s own clients would otherwise reach the sandbox. The server in the sandbox must listen on 127.0.0.1 or 0.0.0.0. One bound only to ::1 or to another address is not reached, and a connection the sandbox closes before sending anything prints nothing is listening on 127.0.0.1:<port> in the sandbox (a server bound only to ::1 or another address is not reached), or it closed the connection. A forward on a sandbox that is stopping, has ended or failed, or whose first instance has stopped or failed does not start: it exits 13 before it prints its start line (Forwarding …, shown below), with an error line that names the next step, such as:
The next step names the sandbox by its sandbox_ id, since once a sandbox ends its name can belong to a new one. It points to adding funds when the sandbox stopped because your API wallet could not pay, otherwise to logs when the first instance started, whatever stopped it, to hedra-cli sandbox gpus when no GPU of the sandbox’s model was free, and otherwise to creating another sandbox. A forward on a sandbox that is still being created, or whose first instance has not started yet, waits up to 15 minutes for it to run before it prints its start line, printing its progress on stderr. A connection made during the wait is closed at once rather than queued, so only the start line means the port forwards. If the sandbox stops or its first instance fails while the forward waits, the forward exits 13 before its start line with the next step, as above; one still starting after 15 minutes also exits 13. A sandbox that is still running although your API wallet could not pay for it is not refused, and forwards as usual until it stops. A connection after it stops ends the forward with exit code 13, not 15, and a line such as:
A sandbox that has already stopped because the wallet could not pay is refused up front with the same next step, like any stopped sandbox. Keep the wallet funded while a forward runs. Once it is listening, port-forward prints this line on stderr:
Anything that can connect to your machine’s loopback address can use your forwarded port. The listener accepts connections from any local user, as ssh -L and kubectl port-forward do, and a web page open in your browser can send requests to it too. A forwarded connection skips the endpoint-token check that protects the sandbox’s url, so the server in the sandbox is the only check left. Forward only a server that requires its own authentication, such as Jupyter with its token, and stop the forward when you are done.
port-forward cannot slow down the server in the sandbox. When the sandbox sends faster than your side takes the data, because your network link is slower than the server, such as a large download, or a local client reads slowly, the connection is reset once up to about 8 MiB is waiting for it, in Hedra’s queue toward you or in hedra-cli’s memory. When 64 MiB is waiting in hedra-cli across every connection, hedra-cli resets the connection holding the most. Copy large files with hedra-cli sandbox cp instead. When Hedra resets a connection that fell behind, hedra-cli prints Hedra’s message followed by its own advice, such as:
When hedra-cli resets one itself, it prints the forwarded connection fell behind and was reset. A refused, dropped or reset connection resets only that connection, and the forward keeps listening. When your workspace is at its limit of open sandbox sessions, each forwarded connection counts as one, and the forward prints once The workspace is at its limit of open sandbox sessions; connections are refused until some close. The forward stops with an error only when a connection fails in a way hedra-cli treats as final:
  • the sandbox is not ready or has stopped (exit code 13): it stopped (for instance because your API wallet could not pay for it), its first instance stopped or failed, or Hedra refused a connection because the sandbox was not ready;
  • the key lost its scope (10), or was revoked (2);
  • Hedra refused the key with a 403 that names no scope (19);
  • the connection to Hedra can never work (19): a proxy answered its CONNECT with a status other than 2xx, 5xx, 408 or 429, Hedra’s certificate is not trusted by your machine, the server at the other end does not speak HTTP/2, or the proxy or connection settings are invalid.
It then stops accepting connections, prints the error line, and exits with that code once its open connections end. A Ctrl-C while they finish closes them at once, and the exit code is still the error’s.

Reach a sandbox’s URL

A sandbox created with --ports serves that port at its url, which needs a token. token mints one:
It answers:
  • token, sent as Authorization: Bearer <token> to url;
  • url;
  • browser_url, a link that opens url in a browser and keeps that browser signed in to the sandbox;
  • expires_at: the end of the sandbox’s --ttl-seconds, or 30 days after the mint, whichever is earlier.
The token and browser_url are credentials: anyone who has either one reaches the sandbox until it expires. rotate-signing-key revokes every token and session of your workspace’s sandboxes within 30 seconds. A shell, stream, forwarded connection or request already open keeps running, and hedra-cli commands mint new sessions with your API key.

Exit codes

API commands exit with hedra-cli’s usual codes: 0 on success, 1 when the API answers with an error (a refused key included), 2 when no credential is found, 3 for a usage error, 4 when the API description cannot be loaded, and 5 when Hedra cannot be reached, or for another failure of hedra-cli’s own. Stream commands exit with a code for each kind of failure, so a script can tell them apart. hedra-cli sandbox <command> --help prints these codes; -h leaves them out. Apart from a usage error or a signal, each failure prints one line on stderr that starts hedra-cli: <class> (<code>): and names the next step, such as:
With an explicit --format json, jsonl or yaml, the line is a JSON object instead, {"error": {...}}, whose error holds class, exit_code, message, reason and trace_id; reason and trace_id are null when Hedra gave none, such as for a failure before Hedra answered; a failure on the sandbox’s stream has a reason but no trace_id. How each stream command uses the table: