> For the complete documentation index, see [llms.txt](https://developer.celigo.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.celigo.com/cli/getting-started/profiles.md).

# Profiles & regions

A **profile** is a named target for one account and environment: one credential — an OAuth sign-in or an API token — plus a base URL and per-profile defaults. One binary, many accounts.

Profiles live in `~/.celigo/config.json`:

```json
{
  "active_profile": "prod",
  "profiles": {
    "prod": {
      "api_token": "abc…xyz",
      "base_url": "https://api.integrator.io",
      "default_format": "table"
    },
    "eu": {
      "api_token": "def…qrs",
      "base_url": "https://api.eu.integrator.io"
    },
    "au": {
      "api_token": "ghi…tuv",
      "base_url": "https://api.au.integrator.io"
    },
    "qa": {
      "api_token": "jkl…wxy",
      "base_url": "https://api.integrator.io",
      "default_format": "json"
    }
  }
}
```

`prod` and `qa` share a base URL because environments do not get their own hostname. Every environment has its own token, so give each one its own profile.

A profile signed in with `celigo login` holds an `oauth` block in place of `api_token`, and an `identity` reference into a top-level `identities` map — see [One credential, one environment](#one-credential-one-environment).

File mode is `0600` on Unix, and every write is one locked read-modify-write (see [How the file is written](/cli/getting-started/configuration.md#how-the-file-is-written)). Tokens are redacted on any CLI output.

## Managing profiles

All profile management happens through `celigo profile <subcommand>`. Full syntax and flags for each subcommand live in the [command reference](/cli/commands/profile.md). In short: `profile add` creates one, `profile use` switches the active profile, `profile list` shows them, and `profile rename` / `profile delete` rename and delete them. [`celigo login`](/cli/commands/login.md) creates a profile too, signed in with OAuth, and [`celigo logout`](/cli/commands/logout.md) signs one out.

There is no `profile edit`. To change a field on an existing profile, either `celigo config set <key> <value>` against it (see the [`config` command reference](/cli/commands/config.md)) or `profile delete` + `profile add` to rebuild it. `celigo config unset <key>` removes a field so its default applies again.

## Profile names

Since celigo-cli 2026.10.1, a profile you do not name is named after the account and environment its credential reaches. `celigo login` without `--profile` (or `CELIGO_PROFILE`) and `celigo profile add` without a name ask `GET /v1/tokenInfo`, and name the profile `<company>-<environment>` — `acme-corp-production`, `acme-corp-sandbox`. The folding rules are in the [login reference](/cli/commands/login.md#which-profile-it-signs-in).

* **A name is only ever suggested.** `login` signs in an existing profile of that name again only when it is bound to the same environment and holds no API token (one that holds a token is refused outright); `profile add` never takes an existing name. Any other collision is refused, and the CLI never suffixes the name or renames another profile: at a terminal it says why and asks for another name (an empty answer gives up); off one it refuses and says how to name the profile yourself.
* **No name without an answer.** When `GET /v1/tokenInfo` fails or names no account or environment, or the account has neither a company nor an owner name, no name can be derived: the CLI asks at a terminal and refuses off one.
* **Renaming the same way.** `celigo profile rename <old>` without a new name names an existing profile after the environment it is bound to — or, when it records none, after the one `GET /v1/tokenInfo` says its own credential reaches, at its own host.
* **Existing profiles keep their names.** Nothing renames a profile on its own.
* **A new profile that is not active says how to use it.** `To use it, pass --profile acme-corp-sandbox, or make it this machine's active profile with celigo profile use acme-corp-sandbox`. Under `profile_pin_policy = strict`, which refuses that `--profile`, the line names only `celigo profile use`, the person's command to run.

A profile name is one path component, because the profile's account index (`~/.celigo/indexes/<name>.json`) and error log (`~/.celigo/logs/<name>/`) are named after it. Since celigo-cli 2026.10.1, `.`, `..`, a name holding `/`, `\`, or a control character, and the names `__proto__`, `constructor`, and `prototype` are refused wherever a profile is created, selected, or written to — `profile add`, `profile use`, `config set` and `config unset`, `login --profile`, and a name typed at a prompt:

```
Invalid profile name '../acme'. Profile names must not be '.' or '..' or contain path separators or control characters. A profile that already has such a name (from before this check) can only be renamed or deleted: celigo profile rename ../acme <newName> (or delete it: celigo profile delete ../acme).
```

A profile an older version created with such a name — `acme/prod`, say — is still listed, and every refusal of it names the way out: `celigo profile rename acme/prod <newName>`, or `celigo profile delete acme/prod`. The rename and the delete change only the config entry; no file is moved or removed for such a name.

## One credential, one environment

Since celigo-cli 2026.10.1, a profile holds one credential — an OAuth session from `celigo login`, or an API token — never both, and every write keeps it that way: `login` refuses a profile that stores an API token, `profile add --login --api-token` is refused, and so is `config set api_token` on a signed-in profile (`celigo logout` first, or put the token in a profile of its own). `--token` and `CELIGO_API_TOKEN` still win over either credential.

A profile is also bound to one environment. A sign-in — and a `profile add` without a name — records the environment `GET /v1/tokenInfo` names: its user (`_userId`) at its API host is the identity, kept with the account id, company, and environment name as labels in the config file's top-level `identities` map, which the profile references:

```json
{
  "active_profile": "acme-corp-production",
  "profiles": {
    "acme-corp-production": {
      "oauth": {
        "access_token": "<access token>",
        "refresh_token": "<refresh token>",
        "expires_at": "2026-10-17T18:30:00.000Z",
        "client_id": "celigo-cli",
        "issuer": "https://integrator.io",
        "scope": "<granted scopes>",
        "resource": "https://celigo-cli.integrator.io",
        "id_token": "<ID token>",
        "identity_verified": true,
        "token_type": "oauth"
      },
      "base_url": "https://api.integrator.io",
      "identity": "5f83a9b2c7d3e8f1a2b3c4d5@api.integrator.io"
    }
  },
  "identities": {
    "5f83a9b2c7d3e8f1a2b3c4d5@api.integrator.io": {
      "base_url": "https://api.integrator.io",
      "user_id": "5f83a9b2c7d3e8f1a2b3c4d5",
      "account_id": "6a1b2c3d4e5f6a7b8c9d0e1f",
      "company": "Acme Corp",
      "environment_id": "7c2d3e4f5a6b7c8d9e0f1a2b",
      "environment": "Production"
    }
  }
}
```

A later `login` into the profile that reaches another environment — another host, or the same account's sandbox — is refused, and its new session revoked, unless `--allow-account-mismatch` rebinds the profile; the binding survives `logout`. See [The environment binding](/cli/commands/login.md#the-environment-binding). A signed-in profile's session is sent only to the host it was issued for; see [Where the session is sent](/cli/commands/login.md#where-the-session-is-sent).

A pre-release build of `celigo login` could leave a profile holding both credentials. Such a profile is refused wherever it is used — `profile list` marks it `auth=oauth+token (refused: keep one)` — and the refusal names the way to keep one:

```
Profile 'acme-corp-production' holds both an API token and an OAuth sign-in, and a profile holds one credential — the two may be different accounts, so neither is used. To keep the API token: celigo logout --profile acme-corp-production. To keep the sign-in: celigo config unset api_token --profile acme-corp-production.
```

## Selecting a profile

A command resolves its profile in three levels: `--profile` > `CELIGO_PROFILE` > the active profile.

```bash
# Per-command, overrides everything
celigo --profile eu integrations list

# Per-session: bind THIS shell, agent session, or CI job to a profile
export CELIGO_PROFILE=eu
celigo integrations list

# Machine-wide default
celigo profile use eu
celigo integrations list
```

The active profile is one machine-wide value in `~/.celigo/config.json`, read fresh by every invocation — so `celigo profile use` in one terminal redirects every un-pinned command in every other terminal and agent session on the machine, and does **not** redirect a session that pins a profile (`--profile` on each call, or `CELIGO_PROFILE`): that session keeps targeting the profile it pinned. Since celigo-cli 2026.9.1 the command says so on stderr and prints the `export CELIGO_PROFILE=<name>` line that binds one session instead. Since celigo-cli 2026.9.5 the note says both — which sessions follow and which do not, with the `celigo config set profile_pin_policy strict` line that refuses such pins (or, once the policy is strict, that they are now refused) — and, when the shell it is typed in has `CELIGO_PROFILE` exported to another profile, that commands run in that shell keep targeting that profile (or are refused) until the variable is unset or re-exported. `CELIGO_PROFILE` is the per-session binding (the `AWS_PROFILE` / `KUBECONFIG` model): a session that must stay on one account sets it once. The `config` commands and `profile show` read and write the selected profile, whichever level selected it.

A `--profile` or `CELIGO_PROFILE` value that names no profile is an error before any request — `Profile 'x' does not exist. Available: a, b (run 'celigo profile list').` — even when `CELIGO_API_TOKEN` or `--token` is set. Until celigo-cli 2026.9.1 an unknown name resolved to an empty profile and failed later with a hint that, followed, wrote the token into the active profile or created the misspelled one. The commands that manage local state never check (`profile`, `config`, `skills`, and `login`, which creates the profile it signs in to) and neither do the offline tree verbs (`status`, `lint`, `resolve`), so `config set … --profile new` can still create a profile; `profile whoami` does check, and so does `logout`, which fails on an unknown profile instead of calling it not logged in.

`celigo login` is the one command that does not fall back to the active profile: without `--profile` or `CELIGO_PROFILE` it signs in to the profile named after the account and environment the sign-in reaches (see [Profile names](#profile-names)).

A blank `--profile ""` — usually an unset shell variable — is refused before any command runs, since celigo-cli 2026.10.1, instead of silently running the command against the active profile:

```
The --profile value is empty. Provide a profile name (this usually means a shell variable was unset), or drop --profile to use the active profile.
```

Every command that writes to the account first says which one, on stderr: `celigo: profile 'acme-sandbox' → api.integrator.io` (profile name and API host). Structural commands (`create`/`update`/`delete`/`set`, clone and install, `push` — a `push --dry-run` included) print it before running. Since celigo-cli 2026.10.1, operational actions such as `flows run` or `jobs cancel` print it at the start too, before their first request; they used to print it right before their first write. Reads print nothing, and so do `profile`, `config`, `skills`, `docs`, and `feedback`. Stdout is untouched, so `--jq` and JSON piping see only the response; confirmation prompts carry the same label (`[profile 'acme-sandbox' → api.integrator.io] Delete flow 123? [y/N]`). There is no opt-out. Off a terminal the prompt cannot be asked, so the command is refused (exit 1) with a message that sends the question to the person — see [Scripting patterns](/cli/using-the-cli/scripting.md#confirmation-prompts-in-scripts-and-ci).

### A profile pinned away from the active one

Since celigo-cli 2026.9.5, a `--profile` or `CELIGO_PROFILE` value that selects a profile **other than** the machine-wide active profile is the one case a read announces too. Every command that targets the pinned profile prints the target line with the divergence in it, once per invocation, on stderr:

```
celigo: profile 'acme-sandbox' → api.integrator.io (pinned with --profile; the active profile is 'acme-prod')
```

The label reads `pinned with CELIGO_PROFILE` when the variable selected the profile, and confirmation prompts carry the same label. A pin that names the active profile adds nothing — it agrees with the switch. The exempt groups (`profile`, `config`, `skills`, `docs`, `feedback`) and the offline tree verbs (`status`, `lint`, `resolve`, the `graph` verbs, and `promote`, which announces its own source and target) print nothing, as before. `login` and `logout` print it, since celigo-cli 2026.10.1.

This line is the shape of a session whose `--profile` did not follow a `celigo profile use` typed in another terminal: the person at the machine believes the session moved with the switch, and it did not. Before celigo-cli 2026.9.5 the bare `celigo: profile 'acme-sandbox' → …` line only confirmed what the session asked for. Treat the parenthesis as a stop sign, and ask which account is meant before the next command.

### Refusing a pinned profile

`profile_pin_policy` is a machine-wide setting, added in celigo-cli 2026.9.5, that turns the announcement into a refusal. To make `celigo profile use` the only way an account gets selected on a machine — the setting for one laptop that implements for several customers — set it to `strict`:

```bash
celigo config set profile_pin_policy strict     # machine-wide; the default is allow
```

Under `strict`, a `--profile` or `CELIGO_PROFILE` value that differs from the active profile fails before any request, exit 1:

```
Profile 'acme-sandbox' (pinned with --profile) is not the active profile ('acme-prod'), and this machine refuses pinned profiles that differ from it (profile_pin_policy = strict). Drop --profile to target 'acme-prod'. If 'acme-sandbox' is the account you mean, a person switches to it with 'celigo profile use acme-sandbox'; an agent must not switch profiles or change profile_pin_policy to get past this refusal.
```

The message says `Unset CELIGO_PROFILE` instead of `Drop --profile` when the variable selected the profile. Both exits keep the person's choice in charge: drop the pin, or have a person switch the active profile.

The policy applies to every command that turns the selected profile into an API target. Since celigo-cli 2026.10.1 it also applies to the local-state commands that act on the selected profile — `config get`, `set`, `unset`, and `show`; `profile show`, `add`, `delete`, and `rename`; `login` and `logout` — so a pinned session can neither run commands against another profile nor read its settings, give it a token or a mode, rename or delete it, or sign it in and out. A name no profile has yet is not a pin away from one, so `config set … --profile <new>` and `login --profile <new>` still create a profile. The other local-state commands refuse any pin that names a profile other than the active one, even one that does not exist — `profile add` included, so a session pinned to a renamed or deleted name cannot create the profile it is pinned to.

Three `profile` commands are exempt. `profile whoami --profile <name>` still answers, because "which account is this profile?" is the question the policy exists to make people ask before they write; `profile list` names what exists; and `profile use` is not a pin but the switch itself, which under `strict` needs a person at a terminal:

```
Switching the active profile to 'acme-sandbox' needs a person at a terminal while profile_pin_policy is strict: run this command in an interactive shell. An agent must not switch profiles on the person's behalf.
```

Renaming the active profile changes `active_profile` machine-wide, so under `strict` `profile rename` of the active profile needs a terminal too (`Renaming the active profile 'acme-prod' changes the active profile machine-wide and needs a person at a terminal while profile_pin_policy is strict: run this command in an interactive shell. An agent must not switch profiles on the person's behalf.`). `skills` and the offline tree verbs are unaffected. `promote --to <name>` judges its target itself: under `strict`, a target profile that is not the active one needs a person at a terminal to type the profile's name back — see [Promote to another account](/cli/local-tree/promote.md). Like the other machine-wide keys, `profile_pin_policy` rejects `--profile`; `config get profile_pin_policy` and `config show` read it back. See the [`config` command reference](/cli/commands/config.md#celigo-config-set).

#### Who can loosen it

The setting protects the person at the machine from a session they are not watching, so that session cannot switch it off. Tightening (`config set profile_pin_policy strict`) never asks. Loosening from `strict` to `allow` — with `config set profile_pin_policy allow`, or with `config unset profile_pin_policy` (since celigo-cli 2026.10.1), which leaves the default `allow` and asks `Unset profile_pin_policy (its default is allow)?` instead — needs a person at a terminal: on one, the command asks them to type `allow` back —

```
Set profile_pin_policy to allow? A --profile or CELIGO_PROFILE that differs from the active profile will no longer be refused on this machine. Type 'allow' to confirm:
```

— and any other answer, or end-of-input, leaves it strict (`Not confirmed; profile_pin_policy stays strict.`, exit 0). When stdin is not a terminal — an agent has none — the command is refused, exit 1:

```
Loosening profile_pin_policy (strict → allow) needs a person at a terminal: run this command in an interactive shell. An agent must not do this on the person's behalf.
```

#### The managed policy file

An agent with a shell can still edit `~/.celigo/config.json`. The managed policy file puts the setting where the account running the CLI — and so an agent running as that account — cannot write:

| Platform     | File                                                                                                                                                                                          |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| macOS, Linux | `/etc/celigo/policy.json` (root-owned)                                                                                                                                                        |
| Windows      | `%ProgramData%\Celigo\policy.json` (administrators). The default `C:\ProgramData\Celigo\policy.json` is tried as well, because `%ProgramData%` is part of the environment a session controls. |

Its whole format is `{"profile_pin_policy": "strict"}`. Create it with administrator rights:

```bash
sudo mkdir -p /etc/celigo && echo '{"profile_pin_policy":"strict"}' | sudo tee /etc/celigo/policy.json
```

The file is **tighten-only**. `strict` there forces `strict` and locks the user-level key:

| Command                                        | Result                                                                                                                                                              |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `celigo config set profile_pin_policy <value>` | Refused: `'profile_pin_policy' is managed by /etc/celigo/policy.json (strict) and cannot be changed with config set; editing that file needs administrator rights.` |
| `celigo config unset profile_pin_policy`       | Refused the same way, naming `config unset`.                                                                                                                        |
| `celigo config get profile_pin_policy`         | `profile_pin_policy = strict (managed by /etc/celigo/policy.json)`                                                                                                  |
| `celigo config show`                           | Adds a `profile_pin_policy_managed_by` entry naming the file.                                                                                                       |
| A refused pin                                  | The refusal reads `(profile_pin_policy = strict, managed by /etc/celigo/policy.json)`.                                                                              |

`allow`, an absent key, or no file leaves the user's setting in charge, so a file planted somewhere a session *can* write changes nothing. The CLI never writes the file. One that cannot be read, is not valid JSON, is not a JSON object, or spells the value wrongly is reported once per invocation on stderr — `warning: ignoring the managed policy file /etc/celigo/policy.json: it is not valid JSON.` — and ignored, so an administrator's typo shows up instead of silently protecting nothing.

### Rules for agents driving the CLI

An agent once carried a stale `--profile` for a day and wrote to the wrong account; the same happened again when the person switched profiles mid-session and the agent's pin did not follow. Another purged a production connection's queue by appending `--yes`, because the `[y/N]` prompt's refusal told it to — whether a person had confirmed was then the agent harness's call, not the prompt's. Six rules prevent both (the `getting-started` [agent skill](/cli/using-the-cli/skills.md) carries them too):

1. **Check the target before the first mutating command of a session.** Run `celigo profile whoami`, and read the `celigo: profile '…' → …` stderr line every write prints. If it is not the account you mean, stop.
2. **Do not pass `--profile` unless the user asked for that profile in this request.** Work on the active profile. A session that must stay on one account is bound once with `export CELIGO_PROFILE=<name>` — a repeated flag is exactly how a stale value survives.
3. **`(pinned with --profile; the active profile is '…')` on a stderr line is a stop sign.** The person switched profiles and your pin did not follow. Ask which account they mean before the next command. Under `profile_pin_policy = strict` the command is refused instead — never run `celigo profile use`, or change `profile_pin_policy`, to get past that; both are the person's decision.
4. **Never run `config set api_token` in response to an error.** `Profile 'x' does not exist` means the NAME is wrong, not the token; writing a token "fixes" it by creating the misspelled profile or overwriting another profile's token.
5. **`celigo profile list` shows what exists.** Pick from it rather than guessing a name.
6. **Never pass `--yes` to a `delete`, a purge, a cancel, or a `--prune` unless the user approved that exact command in this conversation.** Off a terminal the `[y/N]` prompt cannot ask them, so the command is refused (exit 1) and says so. The answer is theirs: quote the command back, wait for it, then re-run with `--yes`. `--yes` exists for scripts and CI pipelines a person set up, not for answering on their behalf. See [Scripting patterns](/cli/using-the-cli/scripting.md#confirmation-prompts-in-scripts-and-ci).

## Regions

| Region | API base URL                   |
| ------ | ------------------------------ |
| US     | `https://api.integrator.io`    |
| EU     | `https://api.eu.integrator.io` |
| AU     | `https://api.au.integrator.io` |
| CA     | `https://api.ca.integrator.io` |

Create one profile per region:

```bash
celigo profile add us --api-token "$US_TOKEN" --api-base-url https://api.integrator.io
celigo profile add eu --api-token "$EU_TOKEN" --api-base-url https://api.eu.integrator.io
celigo profile add au --api-token "$AU_TOKEN" --api-base-url https://api.au.integrator.io
celigo profile add ca --api-token "$CA_TOKEN" --api-base-url https://api.ca.integrator.io
```

Or sign in once per region; each sign-in creates a profile named after the account and environment it reaches:

```bash
celigo login                                           # US
celigo login --base-url https://api.eu.integrator.io   # EU
```

Tokens are **not** interchangeable — a token from one region authenticating against another region's base URL will return `401 Unauthorized`. A sign-in's session is never sent to another region's host: for a signed-in profile, a `--base-url` or `CELIGO_BASE_URL` naming another host is refused before any request.

See [Environments & regions](https://developer.celigo.com/api/getting-started/environments) in the API section for the full context.

## Per-profile config keys

| Field            | Typical use                                                                                               |
| ---------------- | --------------------------------------------------------------------------------------------------------- |
| `api_token`      | API token — the profile's credential when it is not signed in with OAuth. Refused on a signed-in profile. |
| `base_url`       | API base URL; defaults to `https://api.integrator.io`.                                                    |
| `default_format` | `json` for CI, `table` for humans.                                                                        |
| `mode`           | Permission mode: `read`, `operate`, or `full` (default).                                                  |
| `account_alias`  | This account's MCP alias, for the local tree (`profile add --account-alias`).                             |
| `list_fields`    | `default` or `all`: whether `list` commands return the projection or complete documents.                  |

`celigo login` also writes `oauth` (the session) and `identity` (the environment binding); `config set` takes neither, and `celigo logout` removes `oauth`.

Edit any of these with `--profile <name> config set <key> <value>` (`config` writes the selected profile: `--profile`, else `CELIGO_PROFILE`, else active), and remove one with `config unset <key>` so its default applies again — since celigo-cli 2026.10.1 the one way to clear a key, as `config set` refuses an empty value:

```bash
celigo --profile ci config set default_format json
celigo --profile ci config set mode read
celigo --profile ci config unset default_format
```

## Permission modes

Every profile runs in one of three modes; the default is `full`. A mode limits what the CLI sends with the profile's credential — it never adds to what the credential itself may do.

| Mode      | Allows                                                                                                                                                                                                                                                    |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `read`    | Reads: GET requests, the POSTs that only read (listed below), and one PUT that only reads — the RDBMS catalog lookup behind [`metadata types` and `metadata fields`](/cli/commands/metadata.md). Every command that writes is refused before any request. |
| `operate` | Everything `read` allows, plus operational actions — runs, invokes and test runs, error management, debug toggles, job cancellation, data loads, file uploads — and `set` of each group's own operational fields. No structural changes.                  |
| `full`    | Everything: creates, updates, deletes, clones, installs, structural edits, and account administration.                                                                                                                                                    |

`profile`, `config`, `skills`, `login`, and `logout` are never mode-gated.

### Read mode

Since celigo-cli 2026.10.1, read mode refuses every command that writes, before any request is sent — the structural verbs, `set`, and the operational actions alike. Until then the operational actions ran their reads first and were stopped only at their first write, so a run with nothing to do (`flows resolve-errors` with no open errors) exited 0. The refusal says who raises the mode, and where:

```
Command 'flows run' cannot run in read mode (it writes to the account). Current mode: read. A person raises this profile's mode to operate or full at a terminal ('celigo config set mode <mode>') to run operational actions; an agent must not raise it, or switch profiles, to get past this refusal.
```

A write no command-level rule names is still stopped inside the HTTP client, before the request leaves your machine:

```
Read mode: blocked PUT v1/flows/5f83a9b2c7d3e8f1a2b3c4d5. A person raises this profile's mode to operate or full at a terminal ('celigo config set mode <mode>'); an agent must not raise it, or switch profiles, to get past this refusal.
```

A few operations use `POST` but only read or transform data, so `read` mode allows them: marketplace browsing, export previews and `exports invoke` (`exports invoke --connection` included, since celigo-cli 2026.10.1), connection pings, lookup-cache reads, execution-log and EDI transaction queries, the jobs dashboard (`jobs current`, `jobs run-stats`) and `flows run-stats`, signed download URLs, multi-select audit queries, storage queries, `docs search`, and every [processors](/cli/commands/processors.md) transform. None of them create, run, or change anything on the account. Since celigo-cli 2026.10.1, read mode also lets exactly one PUT through: `PUT /v1/connections/<id>/metadata`, the RDBMS catalog lookup behind `metadata types` and `metadata fields` — that path alone, and no other method on it. `feedback submit` works in every mode.

### Operate mode

`operate` runs operational actions and refuses structural changes before any request:

```
Command 'flows create' requires full mode. Current mode: operate. A person raises this profile's mode to full at a terminal ('celigo config set mode full'); an agent must not raise it, or switch profiles, to get past this refusal.
```

Since celigo-cli 2026.10.1, nine verbs that were operate-level need full mode: `environments enable` and `disable` and `workspace-users enable` and `disable` (account administration); `stacks rotate-token` and `on-premise-agents rotate-token` (a credential rotation — the old token stops working the moment the new one is issued); and `storage create-folder`, `storage move`, and `storage copy` (the storage tree's structure, as `storage merge` already was). `storage upload` and `storage replace`, which change a file's bytes, stay operate-level.

`imports invoke` runs a saved import by id in operate mode. Its two other forms write to the destination and need full mode: `imports invoke` without an id, which runs an unsaved import document (since celigo-cli 2026.10.1), and `imports invoke --connection`, which runs one through a connection (added in celigo-cli 2026.10.1). `exports invoke --connection` only reads the source and runs in every mode.

### `set` in operate mode

In operate mode, `set` takes only the group's own operational fields — a resource's on/off switch, its debug windows, and a flow's schedule, where the group's atomic PATCH takes them — so the request is one PATCH and never a GET + PUT of the whole document. Since celigo-cli 2026.10.1 the check is per group; until then a field on the mode's list passed for every group, and one the group could not PATCH went out as a GET + PUT of the whole document.

| Group                                                      | Fields operate mode can `set`                |
| ---------------------------------------------------------- | -------------------------------------------- |
| `flows`                                                    | `disabled`, `logging.debugUntil`, `schedule` |
| `apis`                                                     | `disabled`, `logging.debugUntil`             |
| `mcp-servers`                                              | `disabled`                                   |
| `connections`                                              | `debugDate`, `debugUntil`                    |
| `exports`, `imports`, `scripts`, `ai-agents`, `guardrails` | `debugUntil`                                 |
| `tools`                                                    | `input.debugUntil`, `output.debugUntil`      |
| Every other group                                          | None                                         |

Each field is one value — a flow's `schedule` is one cron string — so a dotted path beneath one (`schedule.cron`, `disabled.foo`) is refused; assign the field whole. The refusal names the fields the group can change, or says it has none:

```
Field(s) not allowed in operate mode: disabled. Allowed fields: debugUntil. Other fields need full mode, which a person raises this profile to at a terminal; an agent must not raise it to get past this refusal.
Field(s) not allowed in operate mode: name. This group has no operate-level fields; set needs full mode.
```

A `null` — `set`'s way of removing a field — that the group's PATCH cannot carry would need a GET + PUT, so since celigo-cli 2026.10.1 operate mode refuses it before any request: `apis set <id> disabled=null` fails with `Field(s) not allowed in operate mode: disabled=null — the apis PATCH cannot remove it, and a removal through a GET + PUT of the whole document needs full mode. …`. Assign a value instead.

### Raising a mode needs a person

Since celigo-cli 2026.10.1, the mode is a boundary a session cannot move for itself. Tightening — `full` to `operate` to `read` — never asks. Raising a profile's stored mode — `config set mode operate` or `full` on a `read` profile, `config set mode full` on an `operate` profile, or `config unset mode` on either, which leaves the default `full` — needs a person at a terminal, who types the new mode back:

```
Set profile 'acme-prod's mode to full? Every session on it is held to read mode now; full mode lets it create, replace and delete resources as well. Type 'full' to confirm:
```

Any other answer leaves the mode as it was (`Not confirmed; profile 'acme-prod' stays in read mode.`, exit 0). Off a terminal — an agent has none — the command is refused, exit 1:

```
Raising profile 'acme-prod's permission mode from read to full needs a person at a terminal: run this command in an interactive shell. An agent must not raise the mode on the person's behalf, nor get past this refusal another way (another profile, an edit of the config file; CELIGO_MODE never raises a profile's mode).
```

The raise is judged against the profile's stored mode, whatever `CELIGO_MODE` says, and again when the write lands. A profile that does not exist yet has the default mode, so `config set mode … --profile <new>` never asks, and `profile add --mode` only ever creates a profile.

### `CELIGO_MODE` only tightens

Since celigo-cli 2026.10.1, the session runs in the stricter of the profile's stored mode and `CELIGO_MODE`: `CELIGO_MODE=read` over a `full` profile is `read`, and `CELIGO_MODE=full` over a `read` profile is still `read`. A profile with no stored mode — or no profile at all, as in env-only CI — has the `full` default, so there the variable sets the ceiling the job runs under. Until then the variable replaced the profile's mode, raising it included.

A refusal says `(from CELIGO_MODE)` only when the variable decided the mode, and then names the variable rather than `config set mode`, which changes nothing while a stricter variable is exported:

```
Command 'flows create' requires full mode. Current mode: read (from CELIGO_MODE). CELIGO_MODE tightens the profile's mode and never raises it: whoever set it in this environment unsets it, or sets it to full; an agent must not change it to get past this refusal.
```

The HTTP client's refusal reads `Read mode (from CELIGO_MODE): blocked …` with the same sentence about the variable.

### A mode that is not a mode

* **A stored `mode` that is not a mode** — a typo, a corrupt file — runs the profile in `read`, since celigo-cli 2026.10.1. Every command warns once (`warning: profile 'acme-prod' stores mode 'readonly', which is not a mode — the profile runs in read mode until a person fixes it with 'celigo config set mode <read|operate|full> --profile acme-prod' at a terminal.`), and `profile list` shows `mode=read (stored 'readonly' is not a mode)`.
* **A `CELIGO_MODE` that is not a mode** counts as `read`, with one warning: `warning: CELIGO_MODE is set to 'readonly', which is not a mode — it counts as read mode until it is unset or set to read, operate, full.` Since celigo-cli 2026.10.1; it used to refuse every command.
* **A `CELIGO_MODE` that is set but empty** — usually an unset shell variable in a template — refuses every command before any request, `config` and `profile` included: `CELIGO_MODE is set but empty (this usually means a shell variable was unset). Unset it to use the profile's mode, or set it to read, operate or full.`

## CI recipes

### GitHub Actions

```yaml
- name: Celigo deploy
  env:
    CELIGO_API_TOKEN: ${{ secrets.CELIGO_PROD_TOKEN }}
    CELIGO_BASE_URL: https://api.integrator.io
  run: |
    npx --yes @celigo/celigo-cli@latest integrations list --format json > integrations.json
```

No profile file needed — pass the env vars. The same holds for a pulled local tree: a token plus `CELIGO_BASE_URL` is a complete identity for `pull`, `diff`, and `push`, because a tree is bound to its account (host and token owner), not to a profile name. See [Workflow](/cli/local-tree/workflow.md).

CI authenticates with an API token: `celigo login` needs a person to approve the sign-in in a browser, and a token in `CELIGO_API_TOKEN` wins over any sign-in stored on the machine.

### GitLab CI

```yaml
deploy-celigo:
  image: node:22
  variables:
    CELIGO_BASE_URL: https://api.integrator.io
  script:
    - npm install -g @celigo/celigo-cli
    - celigo integrations list
```

Set `CELIGO_API_TOKEN` as a masked CI variable.
