camy authenticates with an API key. Sign in once with
camy auth login; after that the key lives
on your machine and every command uses it automatically.
camy auth loginIn CI and other headless environments, set
CAMY_API_KEY instead of signing in.
By default, camy auth login uses a browser device flow.
-
camy starts a sign-in and prints a short code and a verification link to your terminal. Both go to stderr, so
camy auth login >/dev/nullstill shows them:camy — sign in from your browser confirm this code there <CODE> <verification link> ↗ -
camy opens the link in your browser, but only when it is on the same registrable domain as your
api_url. If it is not, camy printssign-in URL … isn't on your api_url's domain — refusing to open it, aborts the sign-in, and points you atcamy auth login --code(the email plus one-time code flow) instead. -
You confirm the code on the page. camy polls in the background until you approve or deny it, showing
waiting for you to clickwithctrl-c cancels · nothing is granted until you clickbeneath it. If the server reports the code expired, camy stops withthat code expired; camy gives up on its own after 11 minutes withsign-in timed out. If the server no longer knows the sign-in at all, camy stops at once withthat sign-in is no longer pending — nothing more will arrive from it; runcamy auth loginagain. -
On approval the server mints an API key with the scopes camy asked for at step 1 — the default set unless you passed
--scopes— and hands it back once. camy stores it and immediately verifies it by calling the account endpoint. A key that gets stored but fails to authenticate is reported as a failure, not a silent success.
If you deny the sign-in, camy auth login exits with the
checkpoint_denied code. If the code expires or you cancel
with Ctrl-C, nothing is granted. If your account already holds as many API
keys as it can, the approval mints nothing: camy prints camy.ai's reason,
such as Maximum of 20 API keys reached — nothing was granted, with the
same exit code as a denial. Revoke keys you no longer use (see
camy keys) and sign in again.
On success, camy prints a card to stderr with who you signed in as, the key's prefix and where it's stored, its scopes, and its expiry (if the server set one):
camy — signed in
────────────────────────────────────────
you <name or email>
key <prefix>… (OS keychain)
scopes chat:read chat:write ...
expires Oct 4 2026 — camy auth login renews it
────────────────────────────────────────
try: camy status
stdout stays empty in this flow — the card is chrome, not data. With
--json, camy prints one object instead, indented and with its keys sorted
alphabetically (the same rendering every --json command uses — see
Scripting with camy):
{
"expires_at": "2026-10-04T00:00:00Z",
"scopes": [
"chat:read",
"chat:write"
],
"status": "signed_in",
"who": "<name or email>"
}camy auth login is interactive by nature and refuses to run under
--no-input, in any combination with other flags:
$ camy auth login --no-input
camy: login is interactive
set CAMY_API_KEY in the environment for headless use
If browser sign-in isn't available on the server you're pointed at, camy falls back to the email-code flow automatically and tells you it's doing so.
See camy auth for the whole sign-in command
group.
On an interactive terminal, the browser sign-in also links this Mac to your
account. One approval gives you two credentials: this terminal's key, stored
as above, and this Mac's own credential, stored the same way. Linking also
makes a signing key on this Mac and keeps it in a file (see
Your Mac as a device). Pass --no-device to sign
in without linking:
camy auth login
camy auth login --no-deviceThe handoff says what the click will also do:
camy — sign in from your browser
confirm this code there <CODE>
<verification link> ↗
that click also links MacBook Air — it can't do anything until you say
⋮ key kept in a file on this Mac · reads only
When you approve, camy prints the signed-in card, then the link, then offers to let Camy read the folder you ran the command in:
✓ linked MacBook Air · key kept in a file on this Mac · reads only
╭──────────────────────────────────────────────────────────────────────────╮
│ GRANT · read one folder MacBook Air │
├──────────────────────────────────────────────────────────────────────────┤
│ Let Camy read ~/Projects/lattice-brief │
│ │
│ reads files, listings, search · nothing runs, nothing changes │
│ scope this folder and beneath it · nothing else on this Mac │
│ later Settings → Your computers · or camy device scope add │
╰──────────────────────────────────────────────────────────────────────────╯
y grant · N skip, the default — the Mac stays linked, does nothing
grant? [y/N]
y grants reading on that folder; if Camy refuses the grant, camy prints
its reason. N, the default, grants nothing, and so does no answer within
two minutes: the Mac stays linked and can do nothing until you add a grant
with camy device scope add or in
Settings. camy doesn't offer the card when you run it from your home folder
or from /. If you deny the sign-in, cancel it, or let the code expire,
nothing is linked either.
camy signs in without linking, and says nothing about linking, when:
- you pass
--no-device, orCAMY_NO_LOCAL=1is set - stdin or stderr isn't a terminal, or you ask for
--json,--jq, or--template - this Mac is already linked; the sign-in then renews only this terminal's key
- you sign in with
--codeor--with-key - the server doesn't offer linking at sign-in
A Mac counts as already linked while it still holds its local link record
and its own credential, until it learns that the link was revoked or that
its credential expired; the resident agent notes either as soon as it hears
of it. After that, camy auth login offers to link it again, and the new
link replaces the old one. Until then, for example right after you revoke
the link on camy.ai while the agent isn't running, run camy device forget
first to link it again, whether through camy auth login or
camy device enroll.
The sign-in completes even when the link doesn't. If the approval comes back without linking this Mac, camy says so and names the way to link it later, or prints Camy's own reason when Camy refused the link:
⋮ MacBook Air wasn't linked — camy device enroll links it
If linking a computer isn't on for your account, it says
MacBook Air wasn't linked — linking a computer isn't on for your account yet
instead.
If Camy refuses the combined sign-in before it starts, camy says so once and runs the plain sign-in:
This Mac won't be linked this time (<reason>) — signing in without it; camy device enroll links it later.
camy auth login --codecamy prompts for your email, sends a one-time code, then prompts for the
code. If your account has two-factor authentication enabled, camy prompts
for an authenticator code or a backup code next, and checks what you type as
either one. If it matches neither, camy stops with
that authenticator or backup code didn't match; run
camy auth login --code again.
Before it mints this terminal's key, camy asks for one more, fresh check:
Minting this terminal's key needs a fresh check that it's you.
If your account uses an authenticator, type its next code, not the one you
just used, or press Enter to get a code by email. Otherwise camy emails you
a second code; camy.ai sends one code a minute, so camy may wait up to a
minute before it sends it. A code that doesn't confirm it's you gets two more
tries. After that camy stops with that code didn't confirm it's you
(exit 3), and nothing is changed.
Like the browser sign-in's key, the key a --code sign-in mints is marked
as this terminal's sign-in key, which is the only kind of key
camy vm logout-everywhere runs with.
Accounts whose only second factor is a passkey (WebAuthn) can't complete a
code sign-in from the terminal — camy detects this and points you at creating
a key at https://camy.ai/p/settings/developer and signing in with
--with-key instead. If code sign-in is disabled on the server you're
pointed at, --code fails with code sign-in is disabled here and the same
hint.
camy auth login --with-keycamy prompts for a key on the terminal (never on stdin), naming
https://camy.ai/p/settings/developer as the place to create one, and checks
that it looks like a camy key (camy_live_… or camy_test_…) before storing
it.
This is the way to install a key that was created elsewhere — for example
one shown once by camy keys rotate.
There is no --api-key flag on any command, by design. Pasting or setting a
key always goes through one of auth login --with-key or the
CAMY_API_KEY environment variable.
Scopes control what a key can do. Every sign-in resolves a set of them — the permissions the stored key carries.
camy auth login --scopes +datasets:write,-jobs:write--scopes applies to the browser flow and --code only. With --with-key
it is ignored: a pasted key already carries whatever scopes it was created
with, and camy reads them back from your key list.
--scopes |
Requests |
|---|---|
| unset | camy's default set — the scopes marked below |
all |
every scope the server registers except device:link |
+add,-remove |
the default set, adjusted |
chat:read,files:read |
exactly that list, replacing the default set |
The two list forms can't be mixed: --scopes +files:read,chat:read is a
usage error (don't mix a bare scope list with +/- adjustments). Any scope
the server doesn't register is rejected as unknown scope when you add it
(+scope) or name it in a bare list. A -scope removal isn't checked — a
name that isn't in the set is silently ignored.
When camy can't fetch the server's list of scopes (you're offline, the
server is older, or its answer is malformed), it uses its own compiled-in
list instead, both to check +scope and bare lists and to expand all.
device:link belongs only to this Mac's own credential, never to a
terminal's key.
--scopes all leaves it out, so signing in with it still links this Mac, and
naming it (+device:link, or in a bare list) is a usage error:
camy: device:link isn't a scope for this terminal's key — it belongs to this Mac's own key
camy auth login links this Mac as it signs in; camy device enroll links it later
| Scope | Covers | Default |
|---|---|---|
chat:read |
Reading chats and their history | ✓ |
chat:write |
Sending chat messages | ✓ |
memory:read |
Reading stored memory | |
memory:write |
Writing stored memory | |
files:read |
Reading files in your workspace | |
files:write |
Writing files in your workspace | |
integrations:read |
Reading integration status | |
integrations:write |
Managing integrations | |
deploys:write |
Publishing sites from Canvas | |
jobs:write |
Creating and managing jobs | ✓ |
tasks:write |
Creating and managing tasks | ✓ |
webhooks:read |
Reading webhook config and deliveries | |
webhooks:write |
Managing webhooks | |
datasets:read |
Reading datasets | |
datasets:write |
Writing datasets | |
data:read |
Reading inbox and feed data | ✓ |
data:actions:draft_email |
Drafting an email as a card action | |
data:actions:create_event |
Creating a calendar event as a card action | |
data:actions:append_memory |
Appending to memory as a card action | |
data:actions:append_dataset |
Appending to a dataset as a card action | |
capture:write |
camy capture |
✓ |
keys:manage |
camy keys self-management |
✓ |
workspace:exec |
Running commands in your cloud workspace: camy vm exec and camy vm shell |
✓ |
The server also registers proactive:write, github:write,
history:read, secrets:read, and secrets:write, none of which a camy
command needs. --scopes all includes them, and +scope accepts them, from
the server's list or camy's own.
Grant only what you need — --scopes all is convenient but broader than
most keys should be.
When a command is refused because the key lacks a scope, camy says
this key lacks a required scope (exit 3, except camy vm exec, which
exits 255), and the hint names the sign-in that adds it, such as
camy auth login --scopes +datasets:write. workspace:exec is in the
default set, but a key minted before it joined doesn't have it, so
camy vm exec and camy vm shell refuse that key; the hint then tells you
to run camy auth login to sign in again.
Signing in again replaces the key this terminal holds rather than piling up a new one every time.
With the default browser flow, every sign-in mints a new key. Once the new
key is stored and verified, camy revokes the key this profile held before,
but only when an earlier camy auth login on this profile stored it:
retired the key this terminal held before (<prefix>…<last4>)
It never retires a key you pasted with --with-key, a key from
CAMY_API_KEY, or a key another machine's sign-in minted.
A key stored by an older camy isn't marked as this profile's sign-in key, so
the first sign-in after you update retires nothing. If the revoke fails, the
sign-in still succeeds, and camy names the camy keys revoke command that
finishes the job. With --json, the signed-in object carries
previous_key_revoked only when camy tried to retire an earlier sign-in key:
true when it revoked it, false when the revoke failed. Otherwise the field
is absent.
With camy auth login --code, camy looks for this machine's key, the one
named exactly camy-cli @ <hostname>. When that key exists, what happens to
it depends on whether you passed --scopes:
- No
--scopes, and the key already holds every scope the sign-in asks for: camy rotates the existing key in place. Rotation keeps the key's current scopes — if that differs from what you'd get from a fresh grant, camy tells you so and points at--scopesto change them. - Explicit
--scopes(even one that matches what's already granted), or a key that lacks one of the default scopes: camy mints a fresh key with exactly the scopes it asked for, namedcamy-cli @ <hostname> (code sign-in <date and time>), and revokes the old key only after the new one exists.
A key that's expired, or one the server won't rotate, gets the same fresh
mint, regardless of --scopes. Either way, the key this machine holds is
never revoked before its replacement exists. If revoking the old key fails,
camy says it is still active and names the camy keys revoke command.
When there is no key with that exact name (a revoked one doesn't count),
--code mints a new key and retires the key this profile held before once
the new one exists, as described above. That is what happens on the sign-in
after a dated mint: the key this machine holds carries the dated name and
the undated key was revoked, so the next --code sign-in mints again rather
than rotating.
A key is stored in your OS keychain, under the service name camy and an
account name scoped to your profile — each profile gets its own
entry.
If the keychain can't be reached, camy falls back to a plain file in your
per-profile state directory, created with permissions 0600 (readable only
by you) and rewritten to 0600 on every write. camy tells you when it falls
back to the file, both at sign-in time and from camy doctor:
! keychain exit status 154 — keys fall back to a 0600 file in the state dir
The keychain, path, and terminal checks in camy doctor are cautions
and never fail the command — the file fallback genuinely works, so a broken
keychain alone never fails camy doctor. Only the auth row (no key found,
or the stored key rejected) fails the command; a rejected key marks the
api row failed alongside it. See
camy doctor and
Troubleshooting.
camy auth statusShows who you're signed in as, the first 12 characters of the key, where it
came from, your api_url, and your active profile:
✓ <name or email> · key <prefix>… · source: profile default · https://api.camy.ai · profile default
camy auth status verifies the key against the account endpoint, so it
needs the network; when a key expiry is on record the line ends with
· expires Oct 4.
The source field names the credential: env when
CAMY_API_KEY supplied the key, or profile <name>
naming the profile whose stored key is in use. --json gives the full
shape, indented and with its keys sorted alphabetically:
{
"api_url": "https://api.camy.ai",
"key_expires": "",
"key_prefix": "<prefix>…",
"key_source": "keychain",
"profile": "default",
"user": { "...": "..." }
}The two are not the same vocabulary: the human line's source: is env or
profile <name>, while the JSON key_source names the store — env,
keychain, or file.
Not signed in is reported as an auth error in both modes, but the shape
differs: in human mode it's the two-line message below on stderr; under
--json it's the standard error object on stderr (see
Scripting with camy). Either way no status object is
printed and stdout stays empty:
$ camy auth status
camy: not signed in
run camy auth login
A key that is stored but no longer works gets its own message instead:
your key expired (or your session expired <date> when camy knows the
date) for an expired key, and this key was revoked — it no longer works
for a revoked one. Both are auth errors (exit 3), and camy auth login
signs this terminal in again.
See camy auth status.
camy auth logoutRemoves the key from this machine — the keychain entry or fallback file — and clears its cached scopes and expiry. This needs no confirmation and always succeeds locally, even if the key was already invalid. It doesn't touch this Mac's link, its signing key, or its own credential; see Take it back for those.
camy auth logout --revokeAlso revokes the key on the server, so it stops working everywhere, not just here. camy finds the key in your key list by both its prefix and its last four characters, revokes that one key, and only then removes it here:
✓ signed out — key revoked and removed from this machine
A key the server already refuses as expired or revoked has nothing left to
revoke: camy removes it here and says the key no longer worked anyway. If
the revoke fails, for example because camy can't reach the API, the key
lacks keys:manage, or camy can't find it among your active keys, camy
revokes nothing, keeps the key stored here so you can try again, and exits
non-zero with the reason, starting didn't revoke this terminal's key
(or, when the key isn't listed,
couldn't find this terminal's key (…) among your active keys). Check with
camy keys list, or revoke by id with camy keys revoke. With --json,
the object says whether it revoked, and adds already_invalid for a key
that was already dead. With no key stored, --revoke has nothing to do: it
asks for no confirmation and signs out like a plain logout.
Because this can't be undone, it asks you to type revoke to confirm (or
pass --confirm revoke in a script) — --force and --no-input alone are
not enough for this one:
camy auth logout --revoke --confirm revokeIf the key currently in play for your profile came from
CAMY_API_KEY rather than a stored profile key, --revoke
refuses outright, before it even asks for confirmation:
camy: CAMY_API_KEY is overriding profile "default" — refusing to revoke it as that profile's key
unset CAMY_API_KEY to revoke the profile's own stored key, or use camy keys revoke
Unset CAMY_API_KEY to revoke the profile's own key, or use
camy keys revoke to revoke a specific key
by id regardless of which one is stored locally.
See camy auth logout.
export CAMY_API_KEY=camy_live_...
camy auth statusSetting CAMY_API_KEY overrides whatever key your active profile would
otherwise use, for the lifetime of the process. It is never written to disk
by camy. This is the supported way to authenticate non-interactively — in
CI, cron, or any script — since auth login itself cannot run headless.
If you've also explicitly chosen a profile (with --profile, CAMY_PROFILE,
or default_profile) while CAMY_API_KEY is set, camy prints a one-time
caution so it's clear which credential actually won:
camy: CAMY_API_KEY overrides profile "work"
The caution is stderr chrome: it is suppressed under --json, --jq, and
--template, and by -q/--quiet. Use camy auth status's key_source
field to detect the env key from a script.
camy keys listcamy keys list lists the API keys on your
account — short id, prefix, name, and when each was created, when it
expires, and when it was last used. Another key on your account that has
expired reads expired in the expires column. The key this terminal holds is marked · this terminal, and it is also the key the
listing is made with, so when it has expired, camy keys list shows no list.
It fails with your key expired or your session expired <date> (exit 3),
and camy auth login fixes it.
camy keys with no subcommand runs the same listing.
camy keys rotate <id>
camy keys revoke <id> [<id>...]rotate replaces a key with a new one carrying the same scopes and prints
the new full key once, to stdout. revoke kills one or more keys server-side
immediately, with a single confirmation for the whole set; it carries on past
an id that fails and exits non-zero if any did. Both take a short id prefix
(at least 4 characters) or a full id, and both ask for confirmation unless
you pass --force. With --json, revoking one key prints one object and
revoking several prints an array with one result per id.
revoke doesn't touch the key stored locally on this machine. When you
rotate the key this terminal holds, the confirmation says so, and camy
stores the replacement in its place, so this terminal keeps working:
this terminal now uses it — kept in the OS keychain
If the old key was this terminal's sign-in key, so is the replacement. With
--json, the rotate object gains stored_locally. A key that came from
CAMY_API_KEY is the exception: camy can't change your environment, so it
says
CAMY_API_KEY still holds the old key — it stops working in 24h; put the new one there.
To use a rotated key on another machine, paste it there with:
camy auth login --with-keySee camy keys and
camy keys rotate.
A profile is a named identity: its own stored key, its own state directory,
and optionally its own api_url/editor/pager overrides in
config.toml. This lets you keep separate accounts — say, personal and
work — side by side, independent of each other.
camy profile
camy profile use workBare camy profile lists default plus every profile that has a
[profile.NAME] table in config.toml, marking the persisted default.
camy profile use NAME only persists the
default choice — it writes default_profile and does not create a
[profile.NAME] table, so the name still won't show up in camy profile's
list until you add such a table with
camy config edit. It also doesn't check
that NAME has a signed-in key; if it doesn't, commands run under it report
"not signed in" until you run camy auth login there too.
Select a profile for a single command with --profile or CAMY_PROFILE:
camy --profile work auth login
CAMY_PROFILE=work camy auth statusSee camy profile and
Configuration for config.toml and the full precedence
ladder.
| Flag | Command | Meaning |
|---|---|---|
--code |
camy auth login |
email plus a one-time code instead of the browser |
--with-key |
camy auth login |
paste an existing key |
--no-device |
camy auth login |
sign in without linking this Mac |
--scopes |
camy auth login |
scope grammar: +add,-remove relative to the default set, a bare list, or all; browser flow and --code only, ignored with --with-key |
--revoke |
camy auth logout |
also revoke the key server-side |
--confirm revoke |
camy auth logout |
confirmation word for --revoke in scripts |
--profile |
any command | run under a named profile instead of the persisted default (env CAMY_PROFILE) |
| Variable | Meaning |
|---|---|
CAMY_API_KEY |
the key to use; wins over the keychain, never persisted by camy |
CAMY_PROFILE |
profile name |
CAMY_API_URL |
API origin, default https://api.camy.ai |
- Configuration —
config.toml, the precedence ladder, and every environment variable - Scripting with camy — the stdout/stderr contract and
--jsonoutput - Exit codes — what
auth,usage, andcheckpoint_deniedmean here - Troubleshooting —
camy doctorand common sign-in problems