# Working in CLI

## Overview

Using the console client [tuna](https://tuna.am/en/docs/guides/install/install-cli.md), you can get secrets, automatically configure the current environment, or pass configuration to the [env](https://en.wikipedia.org/wiki/Environment_variable) of the required application. The console also lets you manage the structure itself — projects, environments, configs and service keys.

| Commands                                                                                | What for                                                     |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| [`list`](#list), [`get`](#get), [`set`](#set), [`delete`](#delete), [`upload`](#upload) | Reading and changing secrets in a config                     |
| [`run`](#running-applications)                                                          | Running an application with secrets in environment variables |
| [`download`](#download), [`substitute`](#substitute)                                    | Exporting secrets to a file or a template                    |
| [`setup`](#setup), [`open`](#open)                                                      | Binding a directory to a config, opening it in a browser     |
| [`projects`](#projects), [`environments`](#environments), [`configs`](#configs)         | Managing the structure and service keys                      |

To get secrets through the tuna console application or [API](https://tuna.am/en/docs/guides/api.md), you need a user [API key](https://my.tuna.am/api_keys) or [Configuration Service Key](https://tuna.am/en/docs/secrets.md#service-keys). For local development, it's more convenient to use global user API keys, but for CI/CD, Production, and other shared environments, we **strongly recommend** using Configuration Service Keys.

## Why is tuna secrets better than direnv for local development?

[direnv](https://direnv.net) is a great utility for local configuration, but it still makes you monitor the local `.env` file, and if new variables appeared in Production, you may not know about it immediately and spend hours on pointless debugging. In tuna secrets, there is [automatic secret synchronization](https://tuna.am/en/docs/secrets.md#secret-synchronization-across-environments), which greatly simplifies this process.

## Why is tuna secrets better than Gitlab/Github/Bitbucket Variables?

Actually, the reason here is the same as with direnv. For example, you have a special project designed for building and publishing an application using [goreleaser](https://goreleaser.com/) and it may require many keys and tokens for different external integrations, while for different environments, and using Gitlab/Github/Bitbucket Variables for this is inconvenient, and it's even more inconvenient to monitor this, plus the complexity in access control. In tuna secrets, access to environments is regulated by RBAC, and you can precisely distribute access only to the necessary people/robots/scripts. And by updating a secret or adding a new one, it will immediately start working in CI.

## Authorization

To access secrets, you need a [Service Key](https://tuna.am/en/docs/secrets.md#service-keys) of a specific configuration or a user [API key](https://my.tuna.am/api_keys). It can be passed through the `--api-key` flag, the `TUNA_API_KEY` environment variable, or saved to the config file with `tuna config save-api-key <your-key>`

The keys differ not only in scope, but also in the set of available commands:

| Key                                | What it can do                                                                                                                                 |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Configuration service key (`tst_`) | Read secrets of its own config: `run`, `download`, `list`, `get`. With `write` access — also `set`, `delete` and `upload` into the same config |
| User API key (`tak_`)              | Everything above plus structure management: `projects`, `environments`, `configs` and issuing service keys                                     |

A service key used against another config returns `403`, and against structure management commands — `401`. So keep a configuration service key in CI/CD, and create projects and configs with a user key.

## Project and configuration

Almost every command works in the context of a project and a configuration. There are three ways to set them, in order of priority:

1. the `--project` (`-p`) and `--config` (`-c`) flags;
2. the `TUNA_SECRETS_PROJECT` and `TUNA_SECRETS_CONFIG` environment variables;
3. a directory bound to a config with the [`setup`](#setup) command.

The `--scope` flag (`.` by default) tells the client which directory's binding to use. See the [`setup`](#setup) section for details.

```shell
tuna secrets list --project api-php --config stage
tuna secrets list -p api-php -c stage
tuna secrets list                              # project and config from the directory binding

```

## Output format

The client separates data from service messages:

* data goes to `stdout`;
* status messages, warnings and errors go to `stderr`.

That is why the output of any command can be redirected to a file or piped without catching extra lines:

```shell
tuna secrets list --only-names > names.txt
tuna secrets list | grep API_

```

### Tables

For humans, commands print a borderless table: headers in caps, an empty cell is `-`, dates are RFC3339 in your local time zone. Such a table is easy to cut with `awk` and does not fall apart in a narrow terminal.

```shell
$ tuna secrets list
NAME             VALUE          TYPE    NOTE  UPDATED
JKLNRFEDUILNNIF  hmac:cea41705  string  -     2026-09-17T11:56:51+04:00

```

### Value fingerprints

By default the `VALUE` column contains not the secret value but its fingerprint, like `hmac:cea41705` — an HMAC-SHA256 of the value keyed with the command's key.

Equal values produce equal fingerprints, and the value cannot be restored from a fingerprint. So fingerprints can be compared across configs ("are the database passwords in stage and prod really different?") and printed into CI logs without disclosing the secret.

Here is what a comparison of two environments looks like: `API_TOKEN` and `API_ENABLED` are the same in dev and prod, while `API_DB` and `LOG_LEVEL` differ — and none of the values had to be disclosed.

***

Real values are shown by [`list --values`](#list) and [`get`](#get):

```shell
$ tuna secrets list --values
NAME             VALUE                             TYPE    NOTE  UPDATED
JKLNRFEDUILNNIF  vcTpFuptQLtJ0beoTQA13rHNeAaGNtjs  string  -     2026-09-17T11:56:51+04:00

```

### JSON and YAML

The `-o` (`--output`) flag with `json` or `yaml` is available on every `tuna secrets` subcommand, including write commands: `set`, `delete`, `upload`, `create`, `rename`, `lock` and the rest print the result in machine-readable form.

```shell
$ tuna secrets set FOO=bar2 -o json
{
  "FOO": {
    "id": 42,
    "name": "FOO",
    "value": "bar2",
    "fingerprint": "hmac:b274b135",
    "value_type": "string",
    "visibility": "masked",
    "override": false,
    "note": "",
    "can_be_converged": false,
    "can_be_promoted": false,
    "reminder": null,
    "created_at": "2026-09-18T06:43:10Z",
    "updated_at": "2026-09-18T06:43:10Z"
  }
}

```

In machine-readable output a secret comes with all its fields and **with the real value** — unlike the table, where a fingerprint is shown by default. Keep that in mind when redirecting such output to a file or a log. The exception is a secret with `restricted` visibility: the server returns neither the value nor the fingerprint, so both fields come back empty.

An empty result is always a valid `[]` or `{}` rather than no output at all, so a script parser needs no special case for it.

### Raw values for scripts

The `--plain` flag of [`get`](#get) and [`configs tokens create`](#service-keys) prints only the value itself, **without a trailing newline**. This is handy for substitution into another command:

```shell
$ docker login -u robot -p "$(tuna secrets get REGISTRY_PASSWORD --plain)" registry.example.com

```

### Errors

Errors are printed to `stderr`, and the exit code is `1`:

```shell
$ tuna secrets list -p test -c nonexistent
Error: Failed to list secrets in test/nonexistent: Config does not exist
  Code: NotFound
  Request ID: 83d3961b1452ab3c7174965acbfbd711

```

With `-o json` the error becomes JSON as well — it can be parsed and, for example, the `request_id` can be attached to a support request:

```json
{"code":"NotFound","error":"Failed to list secrets in test/nonexistent: Config does not exist","request_id":"83d3961b1452ab3c7174965acbfbd711"}

```

## Working with secrets

### List

Lists the secrets of a config. The alias is `ls`.

| Flag           | Description                         |
| -------------- | ----------------------------------- |
| `--values`     | Show values instead of fingerprints |
| `--only-names` | Names only, in a single column      |

```shell
$ tuna secrets list
NAME             VALUE          TYPE    NOTE  UPDATED
JKLNRFEDUILNNIF  hmac:cea41705  string  -     2026-09-17T11:56:51+04:00

```

In the recording the `LOG_LEVEL` fingerprint changes together with the value, and `list --values` then shows the value itself:

***

### Get

Values of one or more secrets. Unlike `list`, `get` shows values right away.

| Flag      | Description                                                   |
| --------- | ------------------------------------------------------------- |
| `--plain` | Values only, without the table and without a trailing newline |

```shell
$ tuna secrets get API_TOKEN API_DB
NAME       VALUE                             TYPE    NOTE  UPDATED
API_DB     postgres://postgres@db:5432/api   string  -     2026-09-17T11:56:51+04:00
API_TOKEN  PwZayIp9NpNntNYd2jkAAQgViM6dXdIm  string  -     2026-09-17T11:56:51+04:00

```

If a requested secret is missing from the config, the command fails and prints nothing — this way a typo in a name does not turn into an empty variable.

### Set

Writes one or more secrets.

| Flag               | Description                                                                                                      |
| ------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `--type`           | Value type: `string`, `json`, `boolean`, `integer`, `decimal`, `email`, `url`, `yaml`, `xml`, `date`, `datetime` |
| `--note`           | A note for the secret                                                                                            |
| `--visibility`     | Value visibility in the web interface: `masked` or `unmasked`                                                    |
| `--no-interactive` | Fail instead of asking for the value interactively                                                               |

The value can be passed in four ways:

```shell
$ tuna secrets set API_TOKEN=PwZayIp9NpNntNYd2jkAAQgViM6dXdIm   # inline
$ tuna secrets set API_TOKEN PwZayIp9NpNntNYd2jkAAQgViM6dXdIm   # as a separate argument
$ openssl rand -hex 32 | tuna secrets set API_TOKEN             # from stdin
$ tuna secrets set API_TOKEN                                    # interactively

```

Interactive input only kicks in for a single name without `=` when nothing arrived on stdin; with `--no-interactive` you get an error instead. The value may be multi-line, a private key for example: the input ends with an empty line followed by a line holding a single period.

Several secrets can be written in one call; the `--type`, `--note` and `--visibility` flags then apply to all of them:

```shell
$ tuna secrets set API_ENABLED=true API_DEBUG=false --type boolean

```

The command prints back the secrets it wrote:

```shell
$ tuna secrets set FOO=bar
NAME  VALUE          TYPE    NOTE  UPDATED
FOO   hmac:bd154f88  string  -     2026-09-18T10:43:10+04:00

```

Do not use the restricted visibility

The `--visibility` flag also accepts `restricted`, but you should not use it for now: the server stops returning the value of such a secret to everyone — including `tuna secrets run` — and `tuna secrets download` starts answering `403` for the whole config. The value cannot be recovered afterwards.

If somebody changed the same secret between the read and the write, the command reports it and fails without overwriting the other change. Repeating the call is enough in that case.

### Delete

Deletes one or more secrets. The alias is `rm`.

```shell
$ tuna secrets delete FOO
NAME       VALUE          TYPE    NOTE  UPDATED
API_TOKEN  hmac:3e96898b  string  -     2026-09-18T10:43:09+04:00
DB_HOST    hmac:829a119e  string  -     2026-09-18T10:43:09+04:00
DB_PORT    hmac:620ef1fa  string  -     2026-09-18T10:43:09+04:00

```

The command prints the config as it remains after the deletion.

If at least one of the listed secrets is missing from the config, nothing is deleted.

### Upload

Uploads secrets from a `.env` or `.json` file.

```shell
$ tuna secrets upload .env
NAME       VALUE          TYPE    NOTE  UPDATED
API_TOKEN  hmac:1ea77bae  string  -     2026-09-18T10:43:11+04:00
DB_HOST    hmac:829a119e  string  -     2026-09-18T10:43:09+04:00
DB_PORT    hmac:620ef1fa  string  -     2026-09-18T10:43:09+04:00
REDIS_URL  hmac:3df78001  string  -     2026-09-18T10:43:11+04:00

```

Like `delete`, the command prints the whole config — after the merge.

The file **adds to** the config: secrets with matching names are overwritten, the rest stay in place. `upload` does not replace the config as a whole.

## Running applications

The `run` command starts your application and passes secrets to it in environment variables. The secrets end up neither in a file nor in the shell history.

```shell
tuna secrets run -- yarn start

```

***

### What the process receives

| Flag                 | Description                                                                                                                                        |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--command`          | Execute a string through the shell: `--command "echo $HOME && env"`                                                                                |
| `--only-secrets`     | Pass only the secrets and a minimum of system variables (`PATH`, `HOME`, `PS1`, `USER`, `SHELL`, `TMPDIR`)                                         |
| `--preserve-env`     | A comma-separated list of names for which an already existing environment value wins; without a value the flag means `true` — preserve all of them |
| `--name-transformer` | Transform names: `upper-camel`, `camel`, `lower-snake`, `lower-kebab`, `tf-var`, `dotnet-env`                                                      |
| `--forward-signals`  | Forward signals to the child process; enabled by default when `stdout` is not a terminal                                                           |

`tuna` returns the exit code of the child process, so the wrapper can be placed into a CI job without breaking its logic.

```shell
$ tuna secrets run --project api-php --config stage -- yarn start
$ tuna secrets run --scope /home/user/src/api -- yarn start
$ tuna secrets run --only-secrets -- env

```

### Automatic restart when a secret changes

If you pass the `--watch` flag, the client subscribes to config changes and restarts the process with the new values.

```shell
tuna secrets run --watch -- yarn start

```

The restart does not happen instantly: the server checks the config once every 30 seconds and only then sends the event. The same delay can pass between a secret change and the application restart — so a couple of dozen quiet seconds mean `--watch` is working, not broken.

The moment of the restart is visible on `stderr`:

```shell
Secrets changed; restarting process

```

In the recording the application prints the value every five seconds: first the old one, then the restart line, then the new one.

***

This video best demonstrates the work:

**VK Video**

**YouTube**

In the video, we run this command:

```shell
tuna secrets run --watch -- bash -c 'while true; do echo -n "$(date -Is) [pid: $$] ==> " ; echo ${API_TOKEN} ; sleep 1; done'

```

### Secrets as a file instead of environment variables

Some applications read configuration only from a file. The `--mount` flag creates a temporary named pipe, puts the secrets into it and passes the path to the child process in the `TUNA_SECRETS_MOUNT_PATH` variable. In this mode the secrets do not go into the environment.

The pipe is created at the given path with `0600` permissions, but the values live only in the client's memory: every open for reading gets a full snapshot and an EOF, and no file with secrets is ever written to disk. Once the process exits, the pipe is removed.

```shell
$ tuna secrets run --mount ./secrets.json --mount-format json -- ./app --config "$TUNA_SECRETS_MOUNT_PATH"

```

| Flag                | Description                                                                |
| ------------------- | -------------------------------------------------------------------------- |
| `--mount`           | Path of the pipe to create                                                 |
| `--mount-format`    | Content format: `env` (default), `env-no-quotes`, `json`, `yaml`, `docker` |
| `--mount-template`  | A template file rendered into the pipe instead of a fixed format           |
| `--mount-max-reads` | How many times the file may be read, `0` for unlimited                     |

***

The `--mount-max-reads N` flag removes the pipe after N reads — handy when exactly one process reads the secrets at startup.

info

Named pipes are not supported on Windows, so `--mount` will not work there.

If the client is killed with `SIGKILL`, the pipe stays on disk and the next run refuses to start with `mount path already exists`. Such a file has to be removed manually.

### Offline mode

`run` can keep the last successfully fetched secrets in an encrypted file and use them when the API is unavailable — the application will not stall because of network problems.

| Flag                           | Description                                                                                                                    |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `--fallback`                   | Path to the file; by default derived from the API address, the key, the project and the config                                 |
| `--fallback-only`, `--offline` | Read only the file, without contacting the API                                                                                 |
| `--fallback-readonly`          | Use the file but never update it                                                                                               |
| `--no-fallback`                | Neither read nor write the file                                                                                                |
| `--fallback-passphrase`        | The passphrase; by default derived from the key, the project and the config. Also read from `TUNA_SECRETS_FALLBACK_PASSPHRASE` |

If the API answers that access is denied or the config is not found (`401`, `403`, `404`), the file is removed: a stale copy of secrets after access revocation is more dangerous than a stopped application. On network errors and `5xx`, on the contrary, the client reads the file and prints a warning.

By default the passphrase is derived from the API key, the project and the config — which means that after a key rotation the old file can no longer be decrypted. If the file has to survive rotation, set the passphrase explicitly: the `--fallback-passphrase` flag takes precedence over the `TUNA_SECRETS_FALLBACK_PASSPHRASE` variable.

Old files can be removed with `run clean`:

```shell
$ tuna secrets run clean --dry-run
Would delete 0 fallback file(s)

$ tuna secrets run clean --max-age 720h
$ tuna secrets run clean --all

```

By default files older than 14 days are removed (`--max-age 336h`).

The first run goes to the API and writes the file along the way, the second one with `--offline` needs no network at all:

***

## Exporting secrets

### Download

Gets the secrets of a config into a file or to standard output.

```shell
tuna secrets download

```

***

| Flag                 | Description                                                                                             |
| -------------------- | ------------------------------------------------------------------------------------------------------- |
| `--format`           | `json` (default), `yaml`, `env`, `env-no-quotes`, `docker`                                              |
| `--no-file`          | Print to standard output instead of writing a file                                                      |
| `--name-transformer` | Transform names: `upper-camel`, `camel`, `lower-snake`, `lower-kebab`, `tf-var`, `dotnet`, `dotnet-env` |
| `--scope`            | The directory whose binding to use                                                                      |

The settings can also be given through [environment variables](https://tuna.am/en/docs/guides/environment-variables.md#secrets).

#### Passing project and configuration

The `--project` and `--config` flags take the name (alias) of the project and configuration as input. By default, the configuration will be saved to the `tuna.json` file in the current directory.

```shell
$ tuna secrets download --project api-php --config stage
Downloaded secrets to tuna.json

$ cat tuna.json
{
  "API_DB": "postgres://postgres:postgres@localhost:5432/postgres",
  "API_TOKEN": "PwZayIp9NpNntNYd2jkAAQgViM6dXdIm"
}

```

#### Passing format

You can specify the format you need for the file.

**json**

```shell
$ tuna secrets download --project api-php --config stage --format json
Downloaded secrets to tuna.json

$ cat tuna.json
{
  "API_DB": "postgres://postgres:postgres@localhost:5432/postgres",
  "API_TOKEN": "PwZayIp9NpNntNYd2jkAAQgViM6dXdIm"
}

```

**yaml**

```shell
$ tuna secrets download --project api-php --config stage --format yaml
Downloaded secrets to tuna.yaml

$ cat tuna.yaml
API_DB: postgres://postgres:postgres@localhost:5432/postgres
API_TOKEN: PwZayIp9NpNntNYd2jkAAQgViM6dXdIm

```

**env**

```shell
$ tuna secrets download --project api-php --config stage --format env
Downloaded secrets to tuna.env

$ cat tuna.env
API_DB="postgres://postgres:postgres@localhost:5432/postgres"
API_TOKEN="PwZayIp9NpNntNYd2jkAAQgViM6dXdIm"

```

An unknown format is an error rather than a silent export of "something".

#### The docker format

The `docker` format prints `KEY=value` pairs without quotes — the same as `env-no-quotes`. Such a file suits `docker run --env-file`, where quotes would become part of the value:

```shell
$ tuna secrets download --format docker --no-file
API_TOKEN=s3cr3t-token-value
DB_HOST=db.internal
DB_PORT=5432

$ tuna secrets download --format docker tuna.env
$ docker run --env-file tuna.env myapp

```

#### Name transformation

The `--name-transformer` flag changes the naming style — useful when the consumer of the secrets does not expect `SCREAMING_SNAKE_CASE`:

```shell
$ tuna secrets download --no-file --format json --name-transformer camel
{
  "apiDb": "postgres://postgres:postgres@localhost:5432/postgres",
  "apiToken": "PwZayIp9NpNntNYd2jkAAQgViM6dXdIm"
}

```

The `dotnet` transformer exists only in `download`: it produces names like `Section:Key`, and a colon is not allowed in an environment variable name. That is why `tuna secrets run` does not offer it — for .NET it has `dotnet-env` with a double underscore.

#### Custom file

You can pass a path in the command to define your own file.

```shell
$ tuna secrets download --project api-php --config stage foo-bar.json
Downloaded secrets to foo-bar.json

```

#### Output to stdout

The `--no-file` flag disables writing to a file and prints the configuration to standard output.

```shell
$ tuna secrets download --project api-php --config stage --no-file --format env
API_TOKEN="PwZayIp9NpNntNYd2jkAAQgViM6dXdIm"
API_DB="postgres://postgres:postgres@localhost:5432/postgres"

```

### Substitute

Renders a template file with the secret values. It fits configs that have their own format: `nginx.conf`, `application.yml`, `.ini` and the like.

The template is a Go [text/template](https://pkg.go.dev/text/template), and the data is a set of name-value pairs:

config.tmpl

```gotemplate
database_url = {{ .DB_URL }}
api_key = {{ index . "API-KEY" }}
extra = {{ index . "FEATURES" | fromjson | tojson }}

```

```shell
$ tuna secrets substitute config.tmpl --output config.ini
$ tuna secrets substitute config.tmpl | kubectl apply -f -

```

***

Without `--output` the result goes to standard output. With `--output` the file is written atomically and with `0600` permissions.

The `{{ index . "DB-NAME" }}` form is needed for names that are not Go identifiers. Besides the standard functions, `tojson` and `fromjson` are available.

If the template mentions a secret that is missing from the config, the command does not substitute an empty value — it exits with code `1`. The message shows both the position in the template and the name itself:

```shell
$ tuna secrets substitute app.conf.tpl
Error: Failed to render template: template: secrets:2:11: executing "secrets" at <.API_TOKN>: map has no entry for key "API_TOKN"

```

For the same reason a `{{ if .DEBUG }}` condition on an optional secret is an error too. Write it through `index` — on a missing key it returns an empty string:

```gotemplate
{{ if index . "DEBUG" }}debug{{ else }}quiet{{ end }}

```

The template in `tuna secrets run --mount-template` is rendered just as strictly — the engine is the same.

## Setting up the environment

### Setup

Binds a directory to a project and a config — after that `--project` and `--config` can be omitted. Bindings are stored in the client [config file](https://tuna.am/en/docs/guides/config-file.md).

```shell
tuna secrets setup

```

Without flags the command offers a list of projects and configs to choose from.

#### Setting up without the menu

As with `download`, in `setup` you can pass `--project` and `--config`:

```shell
$ tuna secrets setup --project api-php --config stage
SCOPE               PROJECT  CONFIG
/home/user/src/api  api-php  stage

```

***

The `--no-interactive` flag (or the `TUNA_SECRETS_NO_INTERACTIVE` variable) forbids asking for missing values — the command fails instead. That is what you want in CI and in install scripts.

#### Setting up a directory other than the current one

If you want to configure a non-current directory, pass the required path to the `--scope` flag:

```shell
tuna secrets setup --project api-php --config stage --scope /home/user/src/api

```

#### The tuna.yaml file in a repository

So that a new developer only needs a single `tuna secrets setup`, put a `tuna.yaml` file into the repository root:

tuna.yaml

```yaml
setup:
  - project: api-php
    config: dev

```

For a monorepo you can describe several bindings, each entry with its own `path` relative to the root:

tuna.yaml

```yaml
setup:
  - project: api-php
    config: dev
    path: backend
  - project: ui
    config: dev
    path: frontend

```

The file is read only when the project and the config are not set by flags or environment variables, and only in the `--scope` directory — the client does not walk up the tree.

#### What a binding looks like

The result of `setup` goes into the client config file:

```shell
$ cat ~/.config/tuna/tuna.yml
secrets:
    scoped:
        /home/user/src/api:
            project: api-php
            config: stage

```

While you are in `/home/user/src/api`, there is no need to pass the project and the config. From another directory either pass them explicitly or point `--scope` at the bound one:

```shell
$ pwd
/home/user/src/worker

$ tuna secrets download --format env --no-file --scope /home/user/src/api
API_TOKEN="PwZayIp9NpNntNYd2jkAAQgViM6dXdIm"
API_DB="postgres://postgres:postgres@localhost:5432/postgres"

```

### Open

Opens the current config in the web interface:

```shell
tuna secrets open

```

If the browser could not be opened, the command prints the link. With `-o json` it prints just the address.

## Managing the structure

Projects, environments and configs can be created and changed right from the console — the same actions as in the [web interface](https://my.tuna.am/secrets).

### Projects

```shell
$ tuna secrets projects
SLUG  NAME  DESCRIPTION  CONFIGS  SECRETS  CREATED
test  test  -            5        12       2026-09-17T11:20:40+04:00

```

| Command                     | Description                                                    |
| --------------------------- | -------------------------------------------------------------- |
| `projects`                  | List of projects                                               |
| `projects get [project]`    | Information about a project                                    |
| `projects create [name]`    | Create a project: `--slug`, `--description`                    |
| `projects update [project]` | Change it: `--name`, `--slug`, `--description`                 |
| `projects delete [project]` | Delete it; without an argument it offers a list to choose from |

```shell
$ tuna secrets projects create "API PHP" --slug api-php --description "Main API"

```

### Environments

```shell
$ tuna secrets environments --project api-php
SLUG   NAME         PERSONAL CONFIGS  CREATED
dev    Development  yes               2026-09-18T10:43:08+04:00
stage  Staging      no                2026-09-18T10:43:09+04:00
prod   Production   no                2026-09-18T10:43:09+04:00

```

| Command                           | Description                               |
| --------------------------------- | ----------------------------------------- |
| `environments`                    | List of the project's environments        |
| `environments get ENVIRONMENT`    | Information about an environment          |
| `environments create NAME`        | Create it: `--slug`, `--personal-configs` |
| `environments rename ENVIRONMENT` | Rename it: `--name`, `--slug`             |
| `environments delete ENVIRONMENT` | Delete an environment                     |

The command aliases are `envs` and `environment`.

### Configs

```shell
$ tuna secrets configs --project api-php
SLUG       ENVIRONMENT  TYPE       LOCKED  CREATED                    LAST FETCH
dev        dev          original   yes     2026-09-17T11:20:40+04:00  2026-09-17T16:20:35+04:00
prod/fghd  prod         duplicate  no      2026-09-17T11:55:14+04:00  2026-09-17T16:17:42+04:00

```

The `TYPE` column tells the environment's main config (`original`) from its copy (`duplicate`), and `LAST FETCH` shows when the secrets were fetched last — it is a good hint of whether anyone uses the config at all.

| Command                   | Description                                                |
| ------------------------- | ---------------------------------------------------------- |
| `configs`                 | List of the project's configs, filtered by `--environment` |
| `configs get [config]`    | Information about a config                                 |
| `configs create [name]`   | Create it in an environment: `--environment`, `--locked`   |
| `configs clone [config]`  | Copy it within its environment: `--name`                   |
| `configs rename [config]` | Rename it: `--name`                                        |
| `configs lock [config]`   | Forbid changes                                             |
| `configs unlock [config]` | Lift the lock                                              |
| `configs delete [config]` | Delete a config                                            |
| `configs logs`            | Change history: `--page`, `--page-size`                    |

#### Change history

```shell
$ tuna secrets configs logs
TIME                       ACTOR    ACTION         SECRET
2026-09-17T16:20:37+04:00  Evgeniy  secret_delete  CLAUDE_ML

```

info

The config change history is currently available for the last three days only — older records are deleted automatically. This is a current limitation, not a plan setting.

#### Service keys

A [service key](https://tuna.am/en/docs/secrets.md#service-keys) is limited to a single config — this is what you should use in CI/CD instead of a user API key.

| Command                    | Description                                                      |
| -------------------------- | ---------------------------------------------------------------- |
| `configs tokens`           | List of the config's keys                                        |
| `configs tokens create`    | Issue a key: `--description`, `--access`, `--max-age`, `--plain` |
| `configs tokens revoke ID` | Revoke a key by its identifier                                   |

| `create` flag   | Description                                                                     |
| --------------- | ------------------------------------------------------------------------------- |
| `--access`      | `read` (default) or `write`                                                     |
| `--max-age`     | Lifetime as a Go duration, for example `720h`; without it the key never expires |
| `--description` | A description to recognize the key by in the list                               |
| `--plain`       | Print only the key itself, without the table and without a trailing newline     |

The key value is shown once — at creation:

```shell
$ tuna secrets configs tokens create --description ci --access read --max-age 720h
Save this token - it will not be shown again
ID  DESCRIPTION  ACCESS  EXPIRES                    CREATED                    TOKEN
5   ci           read    2026-10-18T10:43:31+04:00  2026-09-18T10:43:31+04:00  tst_...

$ tuna secrets configs tokens
ID  DESCRIPTION  ACCESS  EXPIRES                    CREATED
6   ci-plain     read    2026-10-18T10:43:31+04:00  2026-09-18T10:43:31+04:00
5   ci           read    2026-10-18T10:43:31+04:00  2026-09-18T10:43:31+04:00

```

The "Save this token" warning goes to `stderr`, so in a script the key can be captured straight into a variable:

```shell
$ TUNA_API_KEY=$(tuna secrets configs tokens create --description "gitlab ci" --access read --max-age 720h --plain)

```
