Skip to main content

Identity and config

login, logout, whoami and init, plus the three places the CLI looks for a credential.

2 min read
View MarkdownEdit on GitHub

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

RESOLUTION ORDER
First match wins, one value at a time
1 · FLAGS
Root flags on the command
--api-key · --base-url · --project
2 · ENVIRONMENT
Exported variables
CODESPAR_API_KEY · CODESPAR_BASE_URL · CODESPAR_PROJECT
3 · FILE
~/.codespar/config.json
apiKey · baseUrl · project

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.

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.

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

FlagTypeRequiredWhat it does
--api-key <key>stringnoDeclared 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.

--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.

Non-interactive login
printf 'csk_test_probekey\n' | codespar login
Output
stdout + stderr, interleaved
ℹ Get your API key at https://codespar.dev/dashboard/…
API key:
✓ Logged in as ana@loja.com.br
ℹ Organization: Loja Exemplo

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.

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

~/.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.

Refusals

What you seeCauseWhat to do
✗ That doesn't look like a CodeSpar key — they start with `csk_live_` or `csk_test_`., exit 1The 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 1An 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 1The 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 writtenstdin 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.

GEThttps://api.codespar.dev/v1/whoami

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

Human output, line by line

LineComes fromPrinted when the field is absent
Useruser.email, else user.id(unknown)
Organizationorganization.name(none)
Projectproject.name, else project.id(none)
Key envkey.environment(unknown)
Scopeskey.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.

Command
codespar whoami
Output
stdout
User          ana@loja.com.br
Organization  Loja Exemplo
Project       checkout-prod
Key env       test
Scopes        sessions:write, charges:write

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

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 seeCauseWhat to do
✗ Not logged in. Run `codespar login` or set CODESPAR_API_KEY., exit 1No 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 1The 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 1Any 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 1The 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 1The 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.

Command
codespar logout
cat ~/.codespar/config.json
Output
stderr, then the file
✓ Logged out.
{
  "baseUrl": "http://127.0.0.1:8931"
}

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

NameTypeRequiredWhat it does
<name>stringyesDirectory to create, and the value substituted for {{name}}. Must match ^[a-z0-9][a-z0-9-_]*$, case-insensitive.
-t, --template <slug>stringnoOne of the four slugs below. Given, it skips the prompt.
-y, --yesbooleannoSkip the prompt and take the first template, pix-agent.

Templates

SlugLabelFramework
pix-agentPix Payment AgentOpenAI
ecommerce-checkoutE-Commerce CheckoutClaude
streaming-chatStreaming ChatNext.js + Vercel AI
multi-tenantMulti-Tenant AgentNext.js + OpenAI
Command
codespar init loja-pix -y
ls -a loja-pix
Output
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

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 seeCauseWhat to do
✗ Project name must start with a letter/number and contain only letters, digits, dashes, and underscores., exit 1The 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 1The target exists with something in it. An existing empty directory is accepted.Pick another name.
✗ Invalid choice: 9, exit 1The 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 1The 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:

KeyWhat it is
apiKeyThe credential. Written by login, removed by logout.
baseUrlThe API host. Written by login. Read by every command except login itself.
projectThe 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:

VariableEffect
CODESPAR_API_KEYThe credential. Beaten by --api-key.
CODESPAR_BASE_URLThe API host. Beaten by --base-url. Honored by login too, so you can point a whole session at a different environment.
CODESPAR_PROJECTThe project. Beaten by --project, measured on the wire.

Three more shape the output rather than the request:

VariableEffect
NO_COLORSet to anything, and ANSI color is off. Color is also off whenever stdout is not a TTY.
FORCE_COLOR0 turns color off; any other value turns it on even when stdout is piped.
NO_BANNER1 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:

FlagWhat 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.
--jsonMachine-readable output instead of tables. Accepted before or after the subcommand.
-v, --versionPrints 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.

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>.

Exit codes and streams

CodeWhen
0Success, and --help. Also the silent no-op when login reads EOF from a closed stdin.
1An expected refusal, printed to stderr as ✗ <message>. Also Commander's own argument errors, which print as error: <message> without the .
2An 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

Identity and config | CodeSpar