---
title: Identity and config
description: login, logout, whoami and init, plus the three places the CLI looks for a credential.
---

import { Callout } from "fumadocs-ui/components/callout";

Four commands carry everything the `codespar` binary knows about who you are. `login` writes a key to disk, `whoami` proves that key resolves to an org and a project, `logout` removes it, and `init` is the one command in this group that never reads a credential at all.

**Measured 12/09/2026**, against the repository build of the CLI (`packages/cli/dist`, compiled 11/09) pointed at an HTTP stub on `127.0.0.1`. Every transcript below is captured output and no probe reached the production API. Where a line comes from reading the source instead of running it, the sentence says so.

## Where a value comes from

<EventPipeline
  title="RESOLUTION ORDER"
  subtitle="First match wins, one value at a time"
  nodes={[
    { tag: "1 · FLAGS", name: "Root flags on the command", meta: "--api-key · --base-url · --project", tone: "event" },
    { tag: "2 · ENVIRONMENT", name: "Exported variables", meta: "CODESPAR_API_KEY · CODESPAR_BASE_URL · CODESPAR_PROJECT", tone: "validation" },
    { tag: "3 · FILE", name: "~/.codespar/config.json", meta: "apiKey · baseUrl · project", tone: "loop" },
  ]}
/>

Two values leave the chain at `login`. It skips `baseUrl` in the file and resolves the host from the flag, then the variable, then `https://api.codespar.dev`. It also never sees `--api-key`, because the root command and the subcommand declare the same flag.

Three sources, first match wins, resolved one value at a time rather than one source at a time (a key from the environment and a project from the file is a normal combination):

1. Root flags: `--api-key`, `--base-url`, `--project`
2. Environment: `CODESPAR_API_KEY`, `CODESPAR_BASE_URL`, `CODESPAR_PROJECT`
3. The file at `~/.codespar/config.json`

The base URL falls back to `https://api.codespar.dev` when no source carries one. The project has no fallback: without it the server applies the org default.

<Callout type="warn">
There is no `codespar config` subcommand. The top-level help lists 23 entries (22 commands plus Commander's own `help`) and none of them is `config` or `reset`. The file is written by `login` and `logout`, or by hand.
</Callout>

## `codespar login`

Reads an API key from the terminal, spends one `GET /v1/whoami` call proving the key works, and only then writes it to `~/.codespar/config.json` with mode `0600`. A key that fails validation is never persisted.

**Flags**

| Flag | Type | Required | What it does |
|---|---|---|---|
| `--api-key <key>` | `string` | no | Declared on the command and listed by `codespar login --help`, but the value never reaches the command. See the warning below. |

Root flags still apply. `--base-url` is the one that matters here: `login` resolves the host from `--base-url`, then `CODESPAR_BASE_URL`, then `https://api.codespar.dev`. Read from the source: `login` is the one command that does **not** consult `baseUrl` in the config file, so a bare `codespar login` after a login against a staging host rewrites the stored host back to production.

<Callout type="error">
**`--api-key` is listed but not read.** The root program and the `login` subcommand declare the same `--api-key` long flag, and Commander 13.1.0 binds the value to the root command when that happens, while the `login` handler reads only its own options. Measured three ways on 12/09/2026: `codespar login --api-key notakey` prompts; `codespar --api-key notakey login` prompts; and a 12-line isolated Commander program reproduces the split, handing the subcommand `{}` and the root `{"apiKey":"X"}`. Until it is fixed, pipe the key on stdin or export `CODESPAR_API_KEY` and skip `login` entirely.
</Callout>

<Split min={380}>
<SplitPane label="Non-interactive login">

```bash
printf 'csk_test_probekey\n' | codespar login
```

</SplitPane>
<SplitPane label="Output">

```text title="stdout + stderr, interleaved"
ℹ Get your API key at https://codespar.dev/dashboard/…
API key:
✓ Logged in as ana@loja.com.br
ℹ Organization: Loja Exemplo
```

</SplitPane>
</Split>

The dashboard URL is truncated above on purpose: the docs audit rejects any paragraph that pairs that path with key language. Mint keys at [Dashboard → API Keys](https://codespar.dev/dashboard/api-keys).

`API key:` is written to **stdout**, not stderr, so `codespar login > log.txt` swallows the prompt and leaves the terminal looking hung. The `ℹ` and `✓` lines go to stderr.

On a TTY the key is read without echo: the prompt prints once and the typed characters are swallowed, so a pasted key does not land in your scrollback.

**What it writes**

```json title="~/.codespar/config.json, mode 0600"
{
  "apiKey": "csk_test_probekey",
  "baseUrl": "http://127.0.0.1:8931"
}
```

**What it calls** one `GET /v1/whoami`, the same route as [`codespar whoami`](/docs/api/reference/whoami#get-v1whoami).

**Refusals**

| What you see | Cause | What to do |
|---|---|---|
| ``✗ That doesn't look like a CodeSpar key — they start with `csk_live_` or `csk_test_`.``, exit 1 | The value does not start with `csk_`. Checked before any network call, so nothing is written. | Copy the key again; the check is on the prefix only. |
| `✗ API key is required.`, exit 1 | An empty answer at the prompt. Measured by piping a bare newline. | Paste a key, or use `CODESPAR_API_KEY`. |
| `✗ GET /v1/whoami → 401: Invalid API key`, exit 1 | The API rejected the key. Measured against a stub answering 401; the text after the colon is whatever `message` the API returned. Nothing is written: the config directory stayed empty. | Check the key and the environment its project belongs to. |
| Nothing at all, exit 0, nothing written | stdin is closed, which is what a CI step without a TTY looks like. readline reaches EOF, the question never settles and the process ends quietly. Measured with `< /dev/null`. | Pipe the key in, or set `CODESPAR_API_KEY` instead of calling `login`. |

## `codespar whoami`

Spends one `GET /v1/whoami` with the resolved credential and prints the five lines the CLI derives from the response. It is the cheapest way to see which org, project and environment a key actually lands on.

<Endpoint method="GET" path="/v1/whoami" base="https://api.codespar.dev" />

**Flags** none of its own. `codespar whoami --help` lists only `-h, --help`; the root flags do the work.

**Human output, line by line**

| Line | Comes from | Printed when the field is absent |
|---|---|---|
| `User` | `user.email`, else `user.id` | `(unknown)` |
| `Organization` | `organization.name` | `(none)` |
| `Project` | `project.name`, else `project.id` | `(none)` |
| `Key env` | `key.environment` | `(unknown)` |
| `Scopes` | `key.scopes`, joined with a comma | `(all)` |

Each placeholder was measured against a stub that answered `{}`. `(all)` is the CLI's word for "the response carried no `key.scopes` array", not a statement the API made about the key.

<Split min={380}>
<SplitPane label="Command">

```bash
codespar whoami
```

</SplitPane>
<SplitPane label="Output">

```text title="stdout"
User          ana@loja.com.br
Organization  Loja Exemplo
Project       checkout-prod
Key env       test
Scopes        sessions:write, charges:write
```

</SplitPane>
</Split>

With `--json` the response body is printed verbatim, with nothing on stderr:

```bash
codespar whoami --json | jq -r '.key.environment'
```

The flag is declared on the root command but Commander accepts it in both positions, so `codespar whoami --json` and `codespar --json whoami` behave the same.

**Refusals**

| What you see | Cause | What to do |
|---|---|---|
| ``✗ Not logged in. Run `codespar login` or set CODESPAR_API_KEY.``, exit 1 | No key in any of the three sources. Raised before any request. | Run `login`, or export `CODESPAR_API_KEY`. |
| `✗ GET /v1/whoami → 401: Invalid API key`, exit 1 | The API rejected the key. Measured against a 401 stub. | Check that the key matches the environment of its project. |
| `✗ GET /v1/whoami → <status>: <message>`, exit 1 | Any other non-2xx. The client pulls `message`, then `error.message`, then a string `error`, and falls back to the status line alone. Measured at 401 and 403. | Read the message; it is the API's, not the CLI's. |
| `✗ Network error calling GET /v1/whoami: fetch failed`, exit 1 | The host did not answer. Measured against a closed port. | Check `--base-url` and the network. |
| `✗ Request to GET /v1/whoami timed out after 30000ms.`, exit 1 | The per-request timeout. 30s is the client default and this command does not raise it. | Retry, or point at a reachable host. |

## `codespar logout`

Removes the `apiKey` key from `~/.codespar/config.json` and leaves everything else in place. No network call, no credential needed.

**Flags** none. `--json` is accepted and ignored: measured, `codespar logout --json` exits 0 with zero bytes on stdout and `✓ Logged out.` on stderr. A script that parses that output gets an empty string.

<Split min={380}>
<SplitPane label="Command">

```bash
codespar logout
cat ~/.codespar/config.json
```

</SplitPane>
<SplitPane label="Output">

```text title="stderr, then the file"
✓ Logged out.
{
  "baseUrl": "http://127.0.0.1:8931"
}
```

</SplitPane>
</Split>

Two measured edges worth knowing before you trust it:

- On a machine that never logged in, `logout` still creates `~/.codespar/config.json` containing `{}`, with mode `0600`.
- It clears the file, not the environment. With `CODESPAR_API_KEY` exported, the next command is still authenticated. Measured: `whoami` succeeded after `logout` with the key in the environment.

## `codespar init <name>`

Copies one of four templates shipped inside the package into `./<name>` and substitutes `{{name}}` in both file contents and file names. Zero network, and no credential: measured with an empty config file and no `CODESPAR_*` variable set.

**Argument and flags**

| Name | Type | Required | What it does |
|---|---|---|---|
| `<name>` | `string` | yes | Directory to create, and the value substituted for `{{name}}`. Must match `^[a-z0-9][a-z0-9-_]*$`, case-insensitive. |
| `-t, --template <slug>` | `string` | no | One of the four slugs below. Given, it skips the prompt. |
| `-y, --yes` | `boolean` | no | Skip the prompt and take the first template, `pix-agent`. |

**Templates**

| Slug | Label | Framework |
|---|---|---|
| `pix-agent` | Pix Payment Agent | OpenAI |
| `ecommerce-checkout` | E-Commerce Checkout | Claude |
| `streaming-chat` | Streaming Chat | Next.js + Vercel AI |
| `multi-tenant` | Multi-Tenant Agent | Next.js + OpenAI |

<Split min={380}>
<SplitPane label="Command">

```bash
codespar init loja-pix -y
ls -a loja-pix
```

</SplitPane>
<SplitPane label="Output">

```text title="stderr, stdout, then the tree"
ℹ Creating loja-pix using the Pix Payment Agent template...
✓ Created loja-pix/

Next steps:
  cd loja-pix
  cp .env.example .env   # then fill in your keys
  npm install
  npm run dev

.env.example  .gitignore  README.md  package.json  src  tsconfig.json
```

</SplitPane>
</Split>

Without `-t` and without `-y` the command prints the four templates on stdout and asks `Pick a number [1-4] (default 1):`. An empty answer takes the first one.

**Refusals**

| What you see | Cause | What to do |
|---|---|---|
| `✗ Project name must start with a letter/number and contain only letters, digits, dashes, and underscores.`, exit 1 | The name failed the pattern. Measured with `codespar init "my agent"`. | Drop spaces and punctuation. |
| `✗ Unknown template "nope". Available: pix-agent, ecommerce-checkout, streaming-chat, multi-tenant`, exit 1 | `-t` did not match a slug. | Use one of the four. |
| `✗ Directory loja-pix/ already exists and is not empty. Choose a different name or delete it first.`, exit 1 | The target exists with something in it. An existing empty directory is accepted. | Pick another name. |
| `✗ Invalid choice: 9`, exit 1 | The interactive picker got something outside the range. Measured; nothing was created. | Answer with a number in range, or use `-t`. |
| `✗ Template not found at <path>. Is the package installation corrupted?`, exit 1 | The `templates/` directory is missing from the installed package. | Reinstall the CLI. |

## The config file

One file, `~/.codespar/config.json`, created by `login` and rewritten by `logout`. Three keys are read and nothing else:

| Key | What it is |
|---|---|
| `apiKey` | The credential. Written by `login`, removed by `logout`. |
| `baseUrl` | The API host. Written by `login`. Read by every command except `login` itself. |
| `project` | The project every request is scoped to. Nothing in this group writes it; set it by hand or pass `--project`. |

Every write goes through the same helper, which merges into whatever is already on disk and then chmods the file to `0600`, because the file holds a credential. Measured after login: `-rw-------`.

## Environment variables

Three carry credential and scope, and win over the file:

| Variable | Effect |
|---|---|
| `CODESPAR_API_KEY` | The credential. Beaten by `--api-key`. |
| `CODESPAR_BASE_URL` | The API host. Beaten by `--base-url`. Honored by `login` too, so you can point a whole session at a different environment. |
| `CODESPAR_PROJECT` | The project. Beaten by `--project`, measured on the wire. |

Three more shape the output rather than the request:

| Variable | Effect |
|---|---|
| `NO_COLOR` | Set to anything, and ANSI color is off. Color is also off whenever stdout is not a TTY. |
| `FORCE_COLOR` | `0` turns color off; any other value turns it on even when stdout is piped. |
| `NO_BANNER` | `1` suppresses the ASCII banner. The banner already stays away from subcommands, from `--json`, and from any non-TTY stdout. |

## Root flags

Declared once on the root command and valid for every subcommand:

| Flag | What it does |
|---|---|
| `--api-key <key>` | The credential, beating environment and file. Not honored by `login`, per the warning above. |
| `--base-url <url>` | The API host. |
| `--project <id>` | Scopes requests to a project. |
| `--json` | Machine-readable output instead of tables. Accepted before or after the subcommand. |
| `-v, --version` | Prints the version. The short form is `-v`, not Commander's usual `-V`. |

Whatever resolves ends up on the wire: every request carries `Authorization: Bearer <key>`, `Content-Type: application/json` and `User-Agent: codespar-cli/<version>`, plus `x-codespar-project` when a project resolved. Measured with `--project proj_from_flag` alongside `CODESPAR_PROJECT=proj_from_env`: the header carried the flag's value.

<Callout type="warn">
`codespar --version`, the startup banner and the `User-Agent` on every request all report the version the installed package declares — they read the manifest, so they cannot disagree with it or with each other. If you are correlating CLI traffic in your logs, the header is `codespar-cli/<version>`.
</Callout>

## Exit codes and streams

| Code | When |
|---|---|
| `0` | Success, and `--help`. Also the silent no-op when `login` reads EOF from a closed stdin. |
| `1` | An expected refusal, printed to stderr as `✗ <message>`. Also Commander's own argument errors, which print as `error: <message>` without the `✗`. |
| `2` | An unexpected error: the CLI prints `✗ internal error:` and a stack trace. Reserved for a bug. Read from the source; no probe in this group produced one. |

Tables, key/value blocks and JSON go to stdout. The `ℹ`, `✓` and `⚠` lines go to stderr, so `codespar whoami --json | jq` sees only the document. The exception in this group is the `API key:` prompt and the `init` template picker, both of which write to stdout.

## Next steps

<NextStepsGrid items={[
  { label: "COMMANDS", title: "Every command", description: "The rest of the surface: catalog, sessions, connections, money.", href: "/docs/cli/commands" },
  { label: "REFERENCE", title: "GET /v1/whoami", description: "The route login and whoami both call, from the published spec.", href: "/docs/api/reference/whoami" },
  { label: "CONCEPT", title: "Authentication", description: "How API keys, service auth and Connect Links fit together.", href: "/docs/concepts/authentication" },
  { label: "CONCEPT", title: "Projects", description: "What --project scopes, and why a key belongs to one environment.", href: "/docs/concepts/projects" },
]} />
