---
url: https://clusto.app/docs/feature-flags.md
description: >-
  Release flags, Settings > Labs, early access, and the remote flags.json
  kill-switch that control which new behavior runs on a node or cluster.
---

# Feature Flags

A release flag lets Clusto ship new or risky behavior in a build while keeping it
switched off until it has been proven. The old behavior stays in the same binary,
so a flag can be turned off again without a new release. Bug fixes and small
changes ship without a flag.

Flags replace the old pre-release channel. There is one release track, and risky
changes ship behind a flag instead of an unstable build. **Early access** (below)
is what the old unstable channel became: a cluster that wants new behavior first
turns it on in Settings > Labs.

Flags are not a compatibility layer: every node, client and the CLI still run the
same version and speak the same protocol. A flag only decides which behavior runs.

## How a flag is resolved

Each flag resolves to on or off. The first layer that has an opinion wins, highest
first:

1. **Node override** - the `[features]` table in the node's `config.toml`. Ignored
   for cluster-scope flags, so nodes cannot disagree on purpose.
2. **Cluster override** - set per flag in **Settings > Labs**.
3. **Remote kill-switch** - a flag set to `off` in the published `flags.json`.
4. **Early access** - when the cluster has it on, every remaining flag is on.
5. **Remote rollout** - the other states in `flags.json` (see below).
6. **Built-in default** - the default shipped in the build.

### Remote states

A flag in `flags.json` is in one of these states:

| State | Meaning |
| --- | --- |
| `off` | Off for everyone, early access included (the kill-switch) |
| `beta` | Off, except for clusters with early access turned on |
| `rollout` with a percent | On for that share of installs |
| `on` | On for everyone |

A rollout is bucketed locally: the node hashes its cluster token with the flag id
and takes it modulo 100, so every node of one cluster lands in the same bucket. An
unclustered node uses a random install id generated once and stored in its data
directory. Nothing identifying leaves the machine; the node only downloads the
file. Rotating the cluster token reshuffles the buckets.

## Settings > Labs

**Settings > Labs** is the app's view of the flags for the connected cluster:

* **Early access** - a toggle. When on, every flag resolves to on for this
  cluster, whatever its built-in default or remote rollout, unless a per-flag
  override forces it off or the remote file switches it `off`.
* **Per-flag override** - each flag has a Default / On / Off selector. Default
  follows the remote rollout and built-in default; On and Off force the flag for
  the whole cluster.

The settings are shared by every node of the cluster. The last write wins, and a
node that was offline during a change picks it up when it reconnects.

A node whose config still had `[update] channel = "unstable"` is migrated once on
start: early access is turned on for the cluster (only if Labs settings were never
set) and the key is removed from `config.toml`. See
[Remote-URL Updates](/remote-updates#moving-off-the-old-channels).

Both are also in the F1 command palette: **Open Labs settings** and **Turn on
early access** / **Turn off early access**.

## Current flags

| Flag | What it controls | Default |
| --- | --- | --- |
| `plugins.user` | User [plugins](/plugins): the Plugins section in Settings, sidebar views, hooks, agent tools, palette commands and the `clusto plugin` commands (free on every plan) | off |
| `agent_docs.sync` | Automatic Agent Docs sync of user-scope instructions, skills, subagents and commands, with the per-node two-way, receive-only or manual mode. Off, items move only when you send and apply them | off |
| `github.view` | The [GitHub](/github) view | off |
| `azure_devops.view` | The [Azure DevOps](/azure-devops) view | off |
| `time.tracking` | [Time tracking](/time-tracking): the **Time** view at `/time`, the running-timer pill and the Time F1 commands (free on every plan) (cluster scope) | off |
| `gitlab.accounts` | [GitLab](/gitlab) accounts: the **Settings -> GitLab** tab | off |
| `notes.multi_vault` | [Notes](/notes#multiple-vaults): the vault switcher, creating more vaults (a second one needs Pro, Team or a trial) and the per-vault **Kept on** node checklist. Off shows the single first vault as before (app scope) | off |
| `telemetry.usage` | Opt-in [usage data](/usage-data): the consent prompt, **Settings > Privacy** and event upload | off (switched on remotely in `flags.json`) |
| `security.session_credentials` | Limited per-session [credentials](/session-credentials) for sessions that run as a non-root account, instead of the node admin token (node scope) | off |
| `terminal.truecolor` | Starting new terminal sessions with `COLORTERM=truecolor`, so tools that detect color depth draw 24-bit color (node scope) | off |
| `transport.refuse_lan_plaintext` | Refusing credentials over unencrypted connections from networks other than loopback, Tailscale and `[transport] plaintext_networks`; `off` only warns (see [Node Identity & Pairing](/node-identity#unencrypted-connections)) (node scope) | off (switched on remotely once apps have updated) |

## Remote flags file

Clusto publishes `flags.json` next to the update manifest, under the same base URL
and bearer token (`[update].manifest_url` and `token`; see
[Remote-URL Updates](/remote-updates)). A node fetches it on its own schedule,
independent of the update auto-check:

* It polls every `flags_interval_secs` (default `900`, 15 minutes) with a
  conditional request, so an unchanged file costs one small 304 response.
* The last good copy is cached in the node's data directory and used while the
  node is offline. A node with no cache yet uses the built-in defaults.
* A malformed file is ignored: the node keeps the last good set and logs a
  warning.
* Nodes fetch independently, so a cluster-scope flag can differ between nodes for
  up to one interval.

Set `fetch_flags = false` in `[update]` to stop fetching. That removes the remote
kill-switch: only built-in defaults and your overrides apply, and a previously
cached copy is not used either.

A self-hosted mirror must copy `flags.json` along with the manifest, or its nodes
run on built-in defaults. See [Configuration](/configuration#update-and-feature-flag-settings)
for the settings.

### Node override example

```toml
[features]
"github.view" = true
```

Use it to try a flag on one node. It does not apply to cluster-scope flags.

## Publishing flag changes (operators)

Operators who run the distribution use `clusto.ps1`:

```bash
pwsh ./scripts/clusto.ps1 flags                          # list flags and their remote state
pwsh ./scripts/clusto.ps1 flags set <id> off|beta|on     # set a state
pwsh ./scripts/clusto.ps1 flags set <id> rollout 25      # roll out to 25%
pwsh ./scripts/clusto.ps1 flags unset <id>               # drop the remote entry
```

The command validates the id against the flags in the build, shows the change, and
asks before publishing it to the distribution. Add `-Force` to skip the prompt.
Nodes pick up a change within `flags_interval_secs`, so `flags set <id> off` is the
kill-switch for a misbehaving flag.

A typical rollout: ship with the flag off, turn it on for a dev cluster in Labs,
move it to `beta`, then `rollout` 10, 50 and 100, and finally make it the built-in
default in a release. The flag and the old behavior are removed in a later
release.
