Reference · CLI
The authfu CLI
Everything an app needs — creation, configuration, roles, people, groups, agent identities — from the terminal your coding agent already lives in.
The whole loop
authfu login
authfu init # detects the framework, creates the app, writes the auth files
authfu roles add "Editor" --description "Can change content"
authfu roles assign teammate@example.com editorAuthenticate, let authfu init wire the repository up, define a role, and grant it by email — the person does not need an account yet; the role is theirs at first sign-in.
Install
npm install -g @authfu/cli # or: npx @authfu/cli <command>Scopes
A key carries only the scopes it asked for. The default three — profile:read, apps:read, apps:write — deliberately do not include the newer ones, so a key that can create apps cannot by itself grant people access to them, read your audit trail, or establish a SAML federation trust.
authfu login --scope profile:read,apps:read,apps:write,\
groups:read,groups:write,assignments:read,assignments:writeAdd audit:read to read the audit trail, and saml:read,saml:write to manage SAML service providers.
Commands
| Command | What it does |
|---|---|
authfu login | Browser handshake; key stored at ~/.authfu/config.json (0600), revocable in the console. --scope asks for more than the default three. |
authfu init | Wire this repository up: detect the framework, create or reuse the app, write the auth files, the env names, and the gitignore entry. Re-running converges — it never mints a second app or duplicates a redirect URI. --dry-run shows the plan and writes nothing. |
authfu apps | List your applications; * marks the one linked to this directory. |
authfu apps create [name] | Create an app; prints the client ID and one-time secret. --link binds the directory. |
authfu link <client-id> | Bind this directory to an app. The project file holds only the public client ID — commit it. |
authfu set | Update the linked app: --name --description --homepage --logo --redirect-uri… |
authfu roles | List the app’s roles with holder counts. The slug is the exact roles-claim value. |
authfu roles add <name> | Create a role (--description optional). |
authfu roles assign <email> <role> | Grant a role by email — works before the person has signed up. Additive; unknown roles are refused. |
authfu groups | List the groups you administer. groups create, groups delete, and groups members add|remove manage them. |
authfu apps assign <client-id> | Give a group or a person access: --group <name|id> or --user <email>, with --role repeatable. Assigning an address with no account yet returns pending, not a failure. |
authfu apps unassign <client-id> | Remove that grant. Reports whether anything was actually removed. |
authfu apps assignments [client-id] | Who can reach an app and with which roles — groups, people, and pending invitations in one table. |
authfu access explain <email> | Why one person can or cannot reach the app. See below. |
authfu agent login | Device flow for a coding agent: you approve with your authenticator; the token says sub=you, act=the agent. |
authfu agent token / status / logout | Print a valid token for scripting, list sessions, or revoke server-side and forget locally. |
Explaining access
“Why can’t they sign in?” has two very different answers, and the remedies are nothing alike: nobody granted them anything is a setup gap you close with one command, and somebody switched them off is a decision to reverse. Guessing between them is how an afternoon goes into re-assigning an app to a suspended employee. access explain says which it is, and never makes you infer it.
$ authfu access explain teammate@example.com --app app_1
DENIED teammate@example.com -> Fite Camp
cause configuration — nothing grants them access to this app
reason not_invited
detail The app is invite-only and they hold no invitation.
direct no direct assignment
groups no group grants them access
launcher no tile (not_in_launcher)
fix authfu apps assign app_1 --user teammate@example.com --role <role>It reports the door and the launcher tile separately, and flags a MISMATCH when the two disagree rather than averaging them into one confident half-answer. Under --json the provider’s own document rides along untouched, so nothing is lost to a field the CLI did not render.
Machine-readable output
Every command takes --json: exactly one JSON document on stdout and nothing else, with every human word moved to stderr. Errors carry a stable code and a stable exit status, so a script can branch on the failure rather than parse prose. Secrets are redacted from every stream — including an error body that echoes one back.
Writing prompts that use it
Because apps, roles, groups and people are all CLI-reachable, a prompt like “add sign-in with administrator and editor roles, protect /admin for administrators, and make me an administrator” is fully executable by a coding agent — the machine-readable rules live in llms.txt.