> ## Documentation Index
> Fetch the complete documentation index at: https://docs.terma.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up a machine with no browser

> Set up terma on a CI runner, an ephemeral VM, or any machine with no one to sign in, with a team server key in TERMA_API_KEY instead of a browser sign-in.

[`terma setup`](/cli/setup) normally signs you in through your browser. A machine with no one to sign in, such as a CI runner or a VM created for each agent session, signs in with a team server key instead. Setup reads the key from `TERMA_API_KEY` and never opens a browser.

## Create an Ingest only key

<Steps>
  <Step title="Open the team's API keys">
    In Terma, open **Team settings → API keys** for the team the machine reports to.
  </Step>

  <Step title="Create the key">
    Enter a name, for example `ci-runners`, keep **Ingest only** selected, and select **Create key**.
  </Step>

  <Step title="Copy it into your secret store">
    **Copy the key now:** the panel doesn't show it again. Select **Copy key** and store it where the machine reads its secrets, such as a CI secret or the VM's start-up configuration, before you select **Done**.
  </Step>
</Steps>

An Ingest only key can send telemetry into its team but cannot read the team's sessions or metrics ([what each key can do](/concepts/privacy#team-api-keys)). Use one: setup keeps the key on the machine, where anything running there can use it. Setup warns when a key can also read or write the team's data.

Revoking a key cuts off every machine that uses it, so give each pool of machines, such as one CI fleet, its own key.

## Run setup with the key

```bash theme={null}
TERMA_API_KEY=ter_srv_… terma setup --yes --harness claude
```

Setup sets up the key's own team, so there is no organization or team to choose. `--org` and `--team` accept only that organization's and team's ids. `--yes` and `--harness` skip the agent picker: name the agents installed on the machine, `claude`, `codex`, or both.

Setup then works as it does for a person: it fetches the team's collection policy, points your agents at the local relay, and writes their machine-wide hooks. In its summary, **Signed in** names the key by its first characters, for example `server key ter_srv_3f9a…`, and **Server key** says where the key is kept.

<Tip>
  On a VM created for each session, run setup as part of the VM's start-up, before any coding agent starts. An agent that was already running keeps the configuration it started with. Don't run setup while building the VM image or container: every copy made from it would carry the key, in plain text where there is no keychain, and rotating it would mean a rebuild. On a machine with no per-user service manager, as in many containers and VMs, add `--relay-service off`: the relay then starts when an agent needs it, and setup doesn't warn that it couldn't install the background service.
</Tip>

## After setup

Setup keeps the key as the team's key, so your agents' hooks, the relay, and `terma doctor` need no `TERMA_API_KEY` afterwards. The relay keeps the team's collection policy current with the key, as it would with a person's sign-in.

Pass the key to setup alone, for example as an environment variable of the setup step only. While `TERMA_API_KEY` stays set, terma uses it as given instead of the key it kept: `terma doctor` reports `using TERMA_API_KEY`, and `terma teardown --sign-out` refuses. Clear it for those commands; an empty value counts as unset.

Only `terma setup` needs the key again:

| To | Do |
| - | - |
| Repair the machine | Run `TERMA_API_KEY=<the team's server key> terma setup` |
| Rotate the key | Create a new key and put it in the secret your machines start with, run setup with it on every running machine that uses the old one, then revoke the old one |
| Stop using the key on this machine | Run `TERMA_API_KEY= terma teardown --sign-out --yes` ([`terma teardown`](/cli/teardown)) |

`--sign-out` stops the key being the machine's sign-in, and `--yes` skips the confirmation, which a machine with no terminal can't answer. The key stays stored on the machine and keeps working until you revoke it in **Team settings → API keys**. Signing out is not enough before you run code you don't trust on the machine: revoke the key.

Server-key setup creates no key or sign-in in Terma, so a per-session VM you destroy needs no teardown first. Sign-out is for a machine you keep.

## Where the key is kept

Setup keeps the key in the system keychain. On a machine with no usable keychain, which is common on CI runners and minimal VMs, it keeps the key in plain text in `keys.json` in terma's configuration directory (`~/.config/terma` by default, `%APPDATA%\terma` on Windows), readable only by you, and its summary names the file. `--insecure-storage` chooses the file up front.

Setup checks that `TERMA_API_KEY` holds a team server key before sending it anywhere, so a personal token set there by mistake never leaves the machine. The key is sent only to the Terma host it was set up against.

## When setup refuses

| Message | Fix |
| - | - |
| `TERMA_API_KEY is set, but not to a team server key (ter_srv_…)` | Set it to a team server key, or unset it to sign in as a person |
| `the server key in TERMA_API_KEY cannot send telemetry` | Create a key with **Ingest only** selected |
| `check the server key in TERMA_API_KEY: …` | Terma refused the key, or could not be reached. Check that the key was copied in full and is not revoked, and that the machine can reach Terma |
| `the server key in TERMA_API_KEY belongs to no team` | Create the key from the team's **Team settings → API keys** |
| `--org <name>: under a server key, setup cannot look up an organization by name` | Pass the organization's id, or leave `--org` out |
| `TERMA_API_KEY is set — its server key signs in to its own organization` | `--org` names another organization. Leave `--org` out, or unset `TERMA_API_KEY` to sign in to that organization as a person |
| `--team <name>: under a server key, setup cannot look up a team by name` | Pass the team's id, or leave `--team` out |
| `--team <id>: the server key in TERMA_API_KEY is team <id>'s` | `--team` names another team. Leave `--team` out |
| `read this team's current key (…): unlock the system keychain` | Unlock the system keychain, then run setup again |

## Check it

`terma doctor`'s **signed in** check names the key, for example `server key ter_srv_3f9a… in <organization>`. It fails when the machine was set up with a server key but no longer holds one for the team, or when the key was set up against a different Terma host. Either way the fix it names is `TERMA_API_KEY=<the team's server key> terma setup`. Run doctor with `TERMA_API_KEY` cleared: while it is set, the check reports `using TERMA_API_KEY` and doesn't look at the kept key.

## Next step

<CardGroup cols={1}>
  <Card title="terma doctor" icon="stethoscope" href="/cli/doctor">
    Check the whole chain end to end.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.