# CLI reference

Every `gone` command and flag, how it chooses a server, its exit codes and its JSON output. To install `gone` and send a first secret, start with [the CLI guide](https://getgone.dev/docs/cli/index.md).

## Commands

### `gone send`

Encrypts a message and/or files and uploads them.

```usage
gone send [flags]
```

| Flag | What it does |
| --- | --- |
| `-s, --server URL` | Server origin (default: `$GONE_SERVER`, config, `https://app.getgone.dev`) |
| `-t, --ttl DURATION` | Lifetime, e.g. 30m or 24h (default 1h) |
| `-f, --file PATH` | Attach a file; repeat for more (max 10) |
| `--message-file PATH` | Read the message from PATH instead of standard input |
| `--passphrase-prompt` | Protect with a passphrase typed at a prompt |
| `--passphrase-file PATH` | Protect with the passphrase in PATH |
| `--passphrase-generate` | Protect with a generated passphrase (printed to stderr) |
| `--json` | Print the result as JSON |
| `--insecure` | Allow an `http://` server on localhost |
| `--timeout DURATION` | Per-request timeout (default 1m) |

- **The message is read from standard input.** Pipe it in, or type it and press Ctrl-D (Ctrl-Z then Enter on Windows). There is deliberately no flag for the message *text*, so it never lands in shell history or the process list.
- `--message-file PATH` reads the message from a regular file instead, byte for byte (a trailing newline is kept), and standard input is not read at all. Pipes, FIFOs and devices are refused; pipe those into standard input instead.
- To send only files, give standard input nothing: `gone send -f report.pdf </dev/null`.
- The message plus files must fit within the server's size limit (`GONE_MAX_BYTES`, 10 MiB by default); the server also enforces its own minimum and maximum TTL. Independently of the server, `gone` refuses inputs over 64 MiB before encrypting anything (exit 2).
- An attachment's type is guessed from its file extension. Its name is the file's base name.
- **Server policy.** Before reading anything, `gone` fetches the server's policy (`GET /api/policy`, see [Policies](https://getgone.dev/docs/configuration/index.md#policies)). If the server requires a passphrase and no `--passphrase-*` flag is given, or more files are attached than it allows, `gone` exits 2 with error kind `policy` and nothing is read, prompted for or sent. A server older than v3.12 has no policy route and is treated as having no policy. On a server that requires [sign-in](https://getgone.dev/docs/sign-in/index.md), `gone send` and `gone request` stop the same way: the CLI can't sign in through the operator's proxy yet.
- At most one `--passphrase-*` flag may be used. A passphrase must be at least 8 characters. `--passphrase-generate` prints five random words from the EFF short wordlist, joined in CamelCase (about 51 bits), to standard error, so a redirected standard output only captures the link.

Example with files and a passphrase:

```sh
gone send -f id_ed25519 -f id_ed25519.pub --passphrase-generate --ttl 2h </dev/null
```

### `gone get`

Downloads, decrypts and deletes a secret.

```usage
gone get <link|-> [flags]
```

| Flag | What it does |
| --- | --- |
| `-o, --out DIR` | Directory for attachments (default .) |
| `--message-out PATH` | Write the message to the new file PATH instead of standard output |
| `--passphrase-file PATH` | Read the passphrase from PATH instead of prompting |
| `--json` | Print the result as JSON |
| `--raw` | Print the message without escaping control characters |
| `-s, --server URL` | The server you trust (default: `$GONE_SERVER`, config, `https://app.getgone.dev`) |
| `--allow-origin ORIGIN` | Also trust this server for this command; repeat or comma-separate |
| `--insecure` | Allow an `http://` link on localhost |
| `--timeout DURATION` | Per-request timeout (default 1m) |

- The link must point at the server you use, or one you allow with `--allow-origin`; otherwise nothing is sent (see [Which servers a link may reach](#which-servers-a-link-may-reach)).
- The message is written to standard output, attachments to `--out`. The secret is deleted from the server only after everything has been decrypted and written.
- A link of `-` is read from standard input (surrounding whitespace is ignored), so it stays out of the process list and shell history: `gone get - < link.txt`.
- `--message-out PATH` writes the message to a new file instead of standard output, with mode `0600`, byte for byte (`--raw` has no effect on it). An empty message still creates an empty file. The file is written to a temporary name in the same directory, synced, then hard-linked into place, so `PATH` either does not exist or is complete. It is never overwritten: if `PATH` exists, even as a dangling symlink, `get` fails with exit 8. On Windows the mode is not applied; the file inherits the directory's permissions.
- `--out` must be an existing, writable directory. `--message-out` must name a file that does not exist yet, in an existing directory that is writable and supports hard links. Both are checked before the link is opened, so a bad path never burns the secret. The same goes for `--passphrase-file` with a link that has no passphrase (exit 2).
- If writing fails *after* the link was opened (for example, an attachment cannot be saved), whatever this run wrote is removed and the deletion is not confirmed. The secret is still lost: the server has already handed it out once.
- **Existing files are never overwritten.** If `report.csv` exists, the attachment is saved as `report (1).csv`, and so on. Files are created exclusively with mode `0600` inside the output directory, and symlinks are not followed out of it.
- Attachment names are sanitized as in [protocol.md §6.1](https://getgone.dev/docs/protocol/index.md#61-file-names); Windows reserved names (`CON`, `NUL`, …), trailing dots and spaces, and the characters `<>:"|?*` (replaced with `_` on every platform) are also made safe. A leading `.` or `-` is replaced with `_` (`.zshenv` is saved as `_zshenv`), so a sender cannot plant a hidden dotfile or a name that looks like a command-line option.
- For a passphrase-protected link, `gone` prompts on the terminal (up to three tries). The ciphertext is downloaded once and every try runs against that copy. An empty or invalid entry (over 1024 bytes, or not UTF-8) uses up a try, like a wrong passphrase. Before the third try, `gone` warns that it is the last one: the link has already been opened, so a third wrong passphrase loses the secret for good (exit 4, and the error says so). Ctrl-D cancels at once, and because the link has already been opened, cancelling loses the secret. In scripts, use `--passphrase-file`; exactly one trailing newline (`\n` or `\r\n`) is removed from the file. That gives a single try: opening the link has already started the secret's deletion, so a wrong passphrase in the file means the secret is gone, and the error says it can no longer be recovered.
- If the download is cut off after the server handed out the secret, `gone` fetches the same ciphertext again with its claim token, up to twice, while the claim lease lasts (two minutes by default; see [protocol.md §8.2](https://getgone.dev/docs/protocol/index.md#82-claim-get-apisecretid)). Nobody else can fetch it in that time.
- On a terminal, control characters in the message are escaped so a sender cannot inject terminal escape sequences. `--raw` turns this off. When standard output is a pipe or file, the message is written byte-for-byte. With `--json`, DEL, C1 and bidirectional-override characters are always written as `\uXXXX` escapes, so the output is safe on a terminal and still decodes to the original text. (A message that is not valid UTF-8 can't round-trip through JSON: invalid bytes become U+FFFD. Use plain output for binary messages.)

### `gone status`

```usage
gone status <manage-link|-> [-s URL] [--allow-origin ORIGIN]... [--json] [--insecure] [--timeout DURATION]
```

Prints `State: pending`, with creation and expiry times, while the secret is still waiting. A manage link of `-` is read from standard input. Once it has been opened, revoked or has expired, the server answers "not found" (exit 3); it does not reveal which.

### `gone revoke`

```usage
gone revoke <manage-link|-> [-s URL] [--allow-origin ORIGIN]... [--json] [--insecure] [--timeout DURATION]
```

Deletes the secret. Revoking an already-gone secret exits 3. Revoke is reliable only before the recipient opens the link: someone who has already downloaded the ciphertext keeps it.

### `gone request`

```usage
gone request [-l TEXT] [-t DURATION] [-s URL] [--wait [-o DIR] [--message-out PATH] [--raw]] [--json]
```

Asks someone for a secret and prints a **request link** to send them, then exits. A key pair is made on this machine: the private key and the request's manage token are saved on this device (see [Requests on this device](#requests-on-this-device)) and never leave it. The server learns only how long to wait. The person with the secret answers on that link in a browser, or with [`gone reply`](#gone-reply), and only this device can open the reply, once.

- `-l, --label TEXT` is a note so you can tell requests apart. Up to 80 characters are kept; a longer label is cut to 80. It stays on this device and is never sent.
- `-t, --ttl` is how long the link works (default `1h`). A reply is then kept for the same length of time, counted from when it arrives.
- `-s, --server` picks the server as for `send`.
- `--wait` waits for the reply and opens it, taking the same output flags as `request open`. The request is saved first, so Ctrl-C stops waiting but keeps it; resume with `gone request open ID --wait`.
- The request link can't open anything; it only lets someone send you one reply. Send it to the right person.

### `gone request list`

```usage
gone request list [--offline] [--json] [--insecure] [--timeout DURATION]
```

Lists the requests saved on this device, newest first, and asks each one's server for its state:

```text
ID        LABEL       STATE        ASKED                 EXPIRES
3f9a1c2e  staging DB  reply ready  2026-08-10 17:30 UTC  2026-08-10 19:30 UTC
b7d0e411  -           waiting      2026-08-10 17:12 UTC  2026-08-10 18:12 UTC
```

A request the server no longer has (expired, cancelled, or already opened) is shown once as `gone` and then forgotten. If a server can't be reached the state is `unknown`. `--offline` shows the last known state without contacting any server. Checking state is read-only on the server.

### `gone request open`

```usage
gone request open <id> [--wait] [-o DIR] [--message-out PATH] [--raw] [--json] [--insecure] [--timeout DURATION]
```

Downloads the reply, decrypts it with the saved key, and prints the message and saves attachments exactly like [`gone get`](#gone-get). It then deletes the reply from the server and forgets the request here. `<id>` is the full ID or a unique prefix of at least 4 characters. Without `--wait`, a request that has no reply yet exits 3 (`not_ready`); with it, `gone` checks every 10 seconds until the reply arrives or the request expires.

### `gone request cancel`

```usage
gone request cancel <id> [--json] [--insecure] [--timeout DURATION]
```

Cancels the request, or deletes a reply before you open it, and forgets it here. The link stops working at once. Cancelling a request that is already gone exits 3.

### `gone reply`

```usage
gone reply <request-link|-> [-f FILE]... [--message-file PATH] [-s URL] [--allow-origin ORIGIN]... [--json] [--insecure] [--timeout DURATION]
```

Answers someone's request. The link is checked with the server first, so if the request was already answered, cancelled, or has expired, `gone` exits 3 without reading the message. The message comes from standard input or `--message-file`, plus up to 10 attachments (fewer if the server's policy says so), exactly as for `send`. A reply never needs a passphrase, even on a server that requires one for `send`. It is encrypted to the requester's public key from the link: not even you can open it afterwards. Prints `Sent. Only the requester can open it.` and an `Expires:` line with when the reply is deleted if the requester never opens it. A request link of `-` is read from standard input.

Only answer a request link you trust: anyone can make one, so check it came from the person you expect.

### Requests on this device

Requests are saved in `requests.gob` in the `gone` config directory (`~/.config/gone` on Linux, `~/Library/Application Support/gone` on macOS, `%AppData%\gone` on Windows), readable only by you (`0600`, in a `0700` directory). Each entry holds the request ID, its server, your label, the manage token, the private key, the request link and the last known state. That is as private as a browser's site storage, which is where the web UI keeps its requests: anyone who can read the file as you can open a reply that arrives.

The file is internal to `gone`: it is a binary (Go gob) file, not meant to be opened, edited or copied. Use `gone request list` to see what's in it. Every change takes a lock (`requests.lock`), writes a new file and renames it into place, so several `gone` commands running at once never lose a request and the file is never half-written. Like ssh with a private key, `gone` refuses to use the file (exit 8) if other users can read or write it, if it belongs to someone else, or if it is a symlink, and refuses a config directory that others can write to. It says why and prints the `chmod` that fixes it, rather than fixing it silently, because the keys may already have been exposed. (Windows protects your profile with ACLs instead, so these checks don't apply there.) An entry is removed when its reply is opened, the request is cancelled, or it expires. A request made here can only be opened here, and one made in a browser can only be opened in that browser.

### `gone set server`

```usage
gone set server <url> [--insecure]
```

Saves the default server for `send` and `request`. The value must be a bare origin such as `https://gone.example.com`. The file is `gone/config.json` under your user configuration directory (`$XDG_CONFIG_HOME`, or `~/.config` if unset, on Linux; `~/Library/Application Support` on macOS; `%AppData%` on Windows), written with mode `0600`. On Linux and macOS, `gone` refuses to read a config file that other users can write to or that belongs to someone else (exit 8, naming the file), because it decides which server every command trusts. Fix it with `chmod 600` on the file.

`get`, `status`, `revoke` and `reply` use the server named in the link, but only if it is the server you use; see [Which servers a link may reach](#which-servers-a-link-may-reach). `request list`, `request open` and `request cancel` use the server each saved request was made on.

### `gone version`, `gone help`

`gone version` prints the build version. `gone help <command>` prints the help for one command.

## Server selection and transport

`send` and `request` pick their server from, in order: `--server`, the `GONE_SERVER` environment variable, the saved config, then `https://app.getgone.dev`. When `--server` or `GONE_SERVER` overrides a server saved with `gone set server`, a one-line notice on standard error names the server in use (`Using https://… (from GONE_SERVER), not the configured server https://….`).

### Which servers a link may reach

A link names its own server, and anyone can make a link that points anywhere. Opening a link sends a request that tells its server your IP address and when you opened it, so `get`, `status`, `revoke` and `reply` only contact a link's server if it is:

- the server you use, chosen exactly as for `send`: `--server`, `GONE_SERVER`, the saved config, then `https://app.getgone.dev`; or
- named with `--allow-origin ORIGIN` on this command. Repeat the flag or separate origins with commas. Each value must be a bare origin such as `https://gone.example.com`; it applies to this command only.

Any other link is refused before anything is sent, with exit 2 (JSON error kind `untrusted_origin`). The message names both servers, never the link's ID or key:

```text
gone: refused: this link is for https://gone.example.com, but no server is set, so gone only trusts the default https://app.getgone.dev; nothing was sent. If you trust https://gone.example.com, add --allow-origin https://gone.example.com for this command, or make it your server with: gone set server https://gone.example.com
```

If you use your own server, run `gone set server https://your.server` once and its links just work. Origins are compared after lowercasing and dropping a default port, so `https://Gone.Example:443` and `https://gone.example` are the same server.

### Transport

The standard proxy variables (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`) are honored for every `https://` request. An `http://` server (below) is always contacted directly, never through a proxy.

- **HTTPS only.** `http://` origins and links are refused on every command. The one exception is a server on this machine, named `localhost` or a literal loopback address (`127.0.0.0/8` or `::1`), with `--insecure`, for local testing; browsers make the same exception. Other names, including `*.localhost`, are looked up in DNS like any host, so they are refused. Any other `http://` server is refused even with `--insecure`.
- **No redirects.** A redirect is treated as a server error, so a secret is never sent to, or fetched from, an origin other than the one you named.
- **Bounded responses.** Responses larger than the protocol allows are rejected before they are read.

## Exit codes

| Code | Meaning                                                                |
| ---: | ---------------------------------------------------------------------- |
| 0    | Success                                                                |
| 1    | Internal error, or interrupted (Ctrl-C)                                |
| 2    | Usage error: bad flag, bad link, bad server URL, weak passphrase, a link for a server you don't use (`untrusted_origin`), or input the server's policy refuses (`policy`) |
| 3    | Not found: already opened, revoked, expired, or never existed; for `request open` without `--wait`, no reply yet |
| 4    | Wrong passphrase (or the link is damaged); the secret is lost          |
| 5    | Rate limited by the server                                             |
| 6    | Integrity failure: the ciphertext or payload failed authentication     |
| 7    | Network or server error, response too large, or deletion not confirmed |
| 8    | Local I/O error: reading input files, writing attachments or config, or an unsafe config file |

The server cannot tell "already opened", "revoked" and "expired" apart from the outside, so they share code 3. A request with no reply yet is also exit 3, with error kind `not_ready` in JSON, so a script can tell it from `not_found`.

If `get` decrypts a secret but the server does not confirm its deletion, `gone` prints the message and attachments and then exits 7. The server's claim lease expires on its own and the secret is removed by the janitor.

---

## JSON output

With `--json`, results are a single line of JSON on standard output. Times are RFC 3339 in UTC.

`send`:

```json
{"link":"https://…/secret/ID#v1:KEY","manage_link":"https://…/manage/ID#TOKEN","id":"ID","expires_at":"2026-08-10T18:30:00Z"}
```

`get`:

```json
{"message":"hunter2","files":[{"name":"report.csv","path":"out/report (1).csv","type":"text/csv","size":1024}]}
```

This contains the plaintext. Anything that captures standard output (a log, a CI job, an AI agent's context) captures the secret. To keep it out, use `--message-out`.

`get --message-out`: the `message` key is absent, and `message_file` gives the path as you passed it and the size in bytes:

```json
{"message_file":{"path":"creds/restic.pw","size":17},"files":[]}
```

`status`:

```json
{"state":"pending","created_at":"2026-08-10T17:30:00Z","expires_at":"2026-08-10T18:30:00Z"}
```

`revoke`:

```json
{"revoked":true}
```

`request`:

```json
{"id":"ID","link":"https://…/reply/ID#v3:PUBLIC_KEY.FILL_TOKEN","label":"staging DB","expires_at":"2026-08-10T18:30:00Z"}
```

`request --wait` prints **two lines**: the line above as soon as the request is made, then the `request open` result once the reply arrives.

`request list` (an array, newest first; `state` is `waiting`, `ready`, `gone` or `unknown`):

```json
[{"id":"ID","label":"staging DB","state":"ready","server":"https://app.getgone.dev","created_at":"…","expires_at":"…","link":"https://…/reply/ID#v3:…"}]
```

`request open`: the same as `get` (or `get --message-out`). `request cancel`: `{"cancelled":true}`. `reply`: `{"sent":true,"expires_at":"…"}`.

Errors are one line of JSON on **standard error**, and the exit code is still set:

```json
{"error":"rate_limited","message":"rate limited; retry after 30s","retry_after":30}
```

| `error`            | Exit |
| ------------------ | ---: |
| `internal`         | 1    |
| `interrupted`      | 1    |
| `usage`            | 2    |
| `untrusted_origin` | 2    |
| `policy`           | 2    |
| `not_found`        | 3    |
| `not_ready`        | 3    |
| `wrong_passphrase` | 4    |
| `rate_limited`     | 5    |
| `integrity`        | 6    |
| `network`          | 7    |
| `server`           | 7    |
| `too_large`        | 7    |
| `not_confirmed`    | 7    |
| `io`               | 8    |

`retry_after` (seconds) is present only for `rate_limited`, and only when the server supplied it. `--passphrase-generate` still prints the passphrase to standard error as plain text, outside the JSON.
