# Headless Mode



CCP is designed to run unattended in CI pipelines, ephemeral dev VMs, and AI-agent loops. One env var puts the CLI into headless mode, an auth token can be passed in via the environment, and identity flags let you target specific resources without prompts.

## Enabling Headless Mode [#enabling-headless-mode]

Set `CCP_HEADLESS=1` in the environment:

```bash
export CCP_HEADLESS=1
```

This is the single canonical switch. With it set, every interactive prompt is auto-confirmed, every TTY-only command (`ccp db connect`) errors fast, and styled output is replaced with terse one-line `[ccp] ...` logs suitable for log capture.

CCP also auto-detects non-TTY stdin/stderr and behaves the same way. Prefer the env var when running under PTYs (tmux, screen, expect-style wrappers) where TTY detection can falsely report interactive.

Per-command escape hatches still exist — `ccp dev --headless`, `ccp deploy --yes`, `ccp db destroy --yes` — but `CCP_HEADLESS=1` covers all of them at once.

> **Destructive commands run without confirmation in headless mode.** `ccp remove`, `ccp undeploy`, `ccp db destroy`, `ccp db backup delete`, and `ccp domain remove` all execute immediately. This is intentional for automation; double-check the target ID before invoking.

## Authentication [#authentication]

CCP resolves an API credential in this order:

1. `CCP_API_KEY` env var — an organization API key, for organization-level Compute and Sandbox automation
2. `CCP_SESSION_TOKEN` env var — a human or in-VM session token (preferred for headless human automation)
3. The persisted session store — a `0600` file at `~/.ccp/session.json` on macOS (default), or the OS keyring on Linux and Windows (secret service / Credential Manager). There is no fallback from a selected keyring to the file; set `CCP_AUTH_STORE=file` or `=keychain` to override. See [Session storage](/docs/ccp/authentication#session-storage).

`CCP_API_KEY` is a Cluster-issued organization API key. It's held in memory
only — CCP never persists or refreshes it — and it's read as-is, so an
expired or revoked key fails immediately rather than triggering a refresh.
Pair it with `CCP_ORG_ID` so org-scoped commands don't need `--org-id` on
every call:

```bash
export CCP_API_KEY="$ORG_API_KEY"
export CCP_ORG_ID="$ORG_ID"
ccp compute ls
```

An organization API key authorizes **`ccp compute` and `ccp sandbox` only**.
Every other surface — `ccp deploy`, `ccp db`, `ccp domain`, `ccp ci` — still
requires a human session, so those commands need `CCP_SESSION_TOKEN` even
when `CCP_API_KEY` is set.

The recommended headless flow is to authenticate once on a workstation, export the token, and pass it into the runtime environment:

```bash
# On the operator's machine (interactive)
TOKEN=$(ccp auth print-access-token)

# In the headless environment
export CCP_SESSION_TOKEN="$TOKEN"
ccp deploy --prod
```

If `CCP_SESSION_TOKEN` is expired, CCP attempts a silent refresh; if refresh fails, it errors with `Not logged in` and you'll need to re-export a fresh token.

`CCP_API_KEY` behaves differently on failure: since CCP never refreshes it,
a 401 with `CCP_API_KEY` set means the key itself needs to be reminted or
its revocation checked, not re-exported from a workstation session.

`ccp auth login` itself is always interactive — it opens a browser (or, over
SSH/Mosh or on a headless Unix host, prints a device code) and waits for
approval, so it will hang in a non-TTY context. Don't run it in CI or an
agent VM; authenticate once on a workstation and export
`CCP_SESSION_TOKEN` as shown above instead. See
[Authentication](/docs/ccp/authentication#login) for the `--browser` /
`--device` overrides.

## Identity Flags [#identity-flags]

Identity flags name *which* resource a command operates on. They're orthogonal to headless mode — passing `--org-id` doesn't enable headless, and `CCP_HEADLESS=1` won't pick a resource for you.

| Flag              | Used by                                                    | Resolved from                                     |
| ----------------- | ---------------------------------------------------------- | ------------------------------------------------- |
| `--org-id <id>`   | `ccp deploy`, `ccp link`                                   | `.ccp/config.json`, then `CCP_ORG_ID`, if omitted |
| `--app-id <id>`   | `ccp deploy`, `ccp link`, `ccp db create`                  | `.ccp/config.json` if omitted                     |
| `--db-id <id>`    | `ccp db exec`, `migrate`, `connect`, `backup *`, `destroy` | `.ccp/config.json` if omitted                     |
| `--token <token>` | `ccp db exec`, `migrate`, `connect`                        | `.ccp/config.json` if omitted                     |
| `--store-id`      | `ccp store *`                                              | `.ccp/config.json` if omitted                     |

When a required ID is missing and can't be resolved from config, CCP errors clearly rather than guessing. The exception: if exactly one org or store exists on your account, CCP will pick it.

**Commit `cluster.toml`, not `.ccp/config.json`.** `.ccp/` is gitignored (it ignores itself), and it holds this machine's link plus secrets like `database_token`. `cluster.toml` carries the project's shape, so every clone builds the same thing.

> **A fresh checkout does not reattach by name.** The link lives in the gitignored `.ccp/config.json`, so a new clone or CI runner has the project's shape but no App id. A headless deploy in that state resolves the org, then creates a same-named [Project](/docs/ccp/projects) and App — a name match is not treated as identity, so if an App with that name already exists there, the deploy **errors** instead of attaching to it. Pass `--app-id` (and `--org-id`, or set `CCP_ORG_ID`) to select the intended App explicitly.

<Callout type="info">
  App names are unique **across the whole platform**, not just your organization — a name another customer already holds returns `409 name_taken`. Pick something distinctive.
</Callout>

### Resolving the org with `CCP_ORG_ID` [#resolving-the-org-with-ccp_org_id]

Multi-org accounts otherwise have to pass `--org-id` on every org-scoped command (a single-org account auto-picks its only org). Export `CCP_ORG_ID` once and every org-scoped command — `ccp deploy`, `ls`, `link`, `init`, `store`, and the `compute`, `oidc`, and `domain` subcommands — uses it when no higher-precedence hint is present, so a headless multi-org account needs no per-call flag. It's the org-id counterpart to `CCP_SESSION_TOKEN`:

```bash
export CCP_SESSION_TOKEN="$TOKEN"
export CCP_ORG_ID="$ORG_ID"
ccp deploy --prod        # no --org-id, even on a multi-org account
```

The org is resolved with this precedence everywhere an org is chosen:

1. `--org-id` flag
2. project config — `.ccp/config.json` or `cluster.toml`
3. `CCP_ORG_ID`
4. a saved default (`ccp org use`) — see [Organizations](/docs/ccp/organizations)
5. the single org on your account (auto-picked)
6. otherwise: an error in headless mode, or the interactive picker in a TTY

Because the project config outranks the env, a repo's pinned org still wins over an ambient `CCP_ORG_ID` — a cloned project in a `CCP_ORG_ID`-set dev VM deploys where it declares — and an already-linked deploy ignores the env entirely (the linked function's org drives it).

A `CCP_ORG_ID` that matches none of your organizations fails loudly and names the variable:

```
Organization '<id>' (from CCP_ORG_ID) not found — check it or `unset CCP_ORG_ID`
```

so a stale export surfaces as an actionable error instead of a mystery "Organization not found". This loud failure applies even to single-org accounts: a set-but-wrong `CCP_ORG_ID` errors rather than silently falling through to your only org.

**Platform-created dev VMs set it for you.** When Clusterbase provisions a `development` (or build-machine) VM, it injects `CCP_ORG_ID` alongside the in-VM session token, pre-set to your default org — the most-recently-created org you own, or one you're a member of if you own none. A multi-org account's in-VM `ccp deploy` therefore resolves an org with no `--org-id` and no manual export. As precedence 3 above, a project-pinned org or an explicit `--org-id` still overrides it — and it outranks any saved default from `ccp org use`, so don't run that command inside a dev VM expecting it to change VM behavior.

## Reference Matrix [#reference-matrix]

With `CCP_HEADLESS=1` set:

| Command                                                          | Headless-safe     | Required input                                                                                                                                                                                                   |
| ---------------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ccp init <name>`                                                | yes               | positional `<name>`                                                                                                                                                                                              |
| `ccp dev`                                                        | yes               | —                                                                                                                                                                                                                |
| `ccp build`                                                      | yes               | —                                                                                                                                                                                                                |
| `ccp deploy`                                                     | yes               | populated config, or nothing — unlinked, it creates a Project and App, erroring on a name collision (pass `--app-id` to select an existing App; single-org auto-picks; `--org-id` or `CCP_ORG_ID` for multi-org) |
| `ccp project ls`                                                 | yes               | org from flag, config, `CCP_ORG_ID`, or single-org account                                                                                                                                                       |
| `ccp project create <name>`                                      | yes               | positional `<name>` + org resolution                                                                                                                                                                             |
| `ccp project rm <name>`                                          | yes (destructive) | positional `<name>` + `--yes` — refuses to run without it                                                                                                                                                        |
| `ccp link`                                                       | yes               | `--app-id` — org resolves from the usual precedence (single-org accounts need no `--org-id`/`CCP_ORG_ID`)                                                                                                        |
| `ccp list` / `ls`                                                | yes               | linked project; outside one, `--org-id`/`CCP_ORG_ID` or a single-org account                                                                                                                                     |
| `ccp logs`                                                       | yes               | linked project (or `[FUNCTION_ID]`)                                                                                                                                                                              |
| `ccp remove`                                                     | yes (destructive) | linked project                                                                                                                                                                                                   |
| `ccp promote <id>`                                               | yes               | deployment ID                                                                                                                                                                                                    |
| `ccp undeploy <id>`                                              | yes (destructive) | deployment ID                                                                                                                                                                                                    |
| `ccp store create`                                               | yes               | org from config or single-org account                                                                                                                                                                            |
| `ccp store put` / `get` / `ls` / `rm`                            | yes               | store from config or single-store org                                                                                                                                                                            |
| `ccp db create`                                                  | yes               | linked project or `--app-id`                                                                                                                                                                                     |
| `ccp db ls` / `info` / `exec` / `migrate`                        | yes               | linked project or `--db-id`                                                                                                                                                                                      |
| `ccp db destroy <id>`                                            | yes (destructive) | DB ID                                                                                                                                                                                                            |
| `ccp db connect`                                                 | **no**            | use `ccp db exec` instead                                                                                                                                                                                        |
| `ccp db backup create` / `ls`                                    | yes               | linked project or `--db-id`                                                                                                                                                                                      |
| `ccp db backup restore <id>`                                     | yes (destructive) | backup ID                                                                                                                                                                                                        |
| `ccp db backup delete <id>`                                      | yes (destructive) | backup ID                                                                                                                                                                                                        |
| `ccp domain ls` / `add` / `verify` / `link` / `unlink`           | yes               | flags as needed                                                                                                                                                                                                  |
| `ccp domain remove <domain>`                                     | yes (destructive) | domain                                                                                                                                                                                                           |
| `ccp auth login`                                                 | **no**            | interactive only; export `CCP_SESSION_TOKEN` instead                                                                                                                                                             |
| `ccp auth print-access-token` / `export-access-token` / `logout` | yes               | logged-in session                                                                                                                                                                                                |

`ccp db connect` and `ccp auth login` are the commands that need a human:
`ccp db connect` opens a TUI psql shell (use `ccp db exec '<SQL>'` for
one-off statements), and `ccp auth login` always waits for browser or device
approval (export `CCP_SESSION_TOKEN` for headless auth instead).

## Typical Agent Workflow [#typical-agent-workflow]

```bash
# In an ephemeral dev VM (CCP_HEADLESS=1, CCP_SESSION_TOKEN, and CCP_ORG_ID already in env)

cd /workspace
ccp init my-app --template react
cd my-app

# ... agent edits code, runs tests ...

# First deploy creates a Project + App and links them (the VM's injected
# CCP_ORG_ID resolves your default org with no flag; pass --org-id to target
# a different org), and writes the ids to config:
ccp deploy --prod

# Persist the project shape before VM teardown. The link in .ccp/ is
# gitignored and per-machine:
git add cluster.toml
git commit -m "commit project shape"
git push

# Later, in a fresh VM with the same repo. cluster.toml supplies the shape,
# but a headless deploy no longer reattaches by name — pass --app-id (and
# --org-id) to target the original App if one already exists there:
git clone "$REPO" && cd my-app
ccp deploy --prod --org-id "$ORG_ID" --app-id "$APP_ID"
```

## Reference for AI Agents [#reference-for-ai-agents]

`ccp print-skill` prints an authoritative reference for the CLI when used by an AI agent. It's embedded in the binary, so it stays in sync with the version of `ccp` you have installed.

`print-skill` is progressive — load just the part you need instead of the whole reference:

```bash
ccp print-skill          # compact overview: headless mode, auth, project shape,
                         #   the reference matrix, and an index of topics
ccp print-skill <topic>  # one surface in depth, e.g. `ccp print-skill db`
ccp print-skill all      # the complete reference — every topic at once
```

Start with `ccp print-skill`, then fetch the one or two topics your task needs. The available topics are `deploy`, `stores`, `oidc`, `db`, `domains`, `compute`, `ci`, and `env`; an unknown topic errors and lists the valid ones. The overview always carries the reference matrix and a generated topic index, so an agent can discover every surface even before fetching a deep dive.

Each view is a markdown document covering authentication, project shape, the relevant commands' headless behavior, and common pitfalls. `ccp print-skill all` reproduces the full reference (the original behavior) — pipe it into an agent's context, or save it as `SKILL.md` in your project to give a coding agent a complete reference for working with CCP.

## Common Pitfalls [#common-pitfalls]

* **Don't commit:** `.env`, `node_modules/`, or `.ccp/index.js` (build output).
* **Always commit:** `cluster.toml` — the project's shape, so every clone builds the same thing. Not `.ccp/config.json`: it's gitignored, per-machine, and can hold a live `database_token`.
* **Token expiry:** if `CCP_SESSION_TOKEN` is expired and silent refresh fails, re-export a fresh token from the operator's machine. `CCP_API_KEY` is never refreshed by CCP at all — a 401 means remint or revocation-check the key itself.
* **No git integration:** CCP doesn't commit or push for you. Commit `cluster.toml` manually after the first deploy or link, and carry the org id into the next VM (`--org-id` or `CCP_ORG_ID`); a name match no longer reattaches the App, so pass `--app-id` to target it explicitly.
* **Build artifacts** (`.ccp/index.js`, bundled assets) regenerate on each `ccp deploy` / `ccp build` — safe to delete from a clean checkout.
